writeKtx2 function

Uint8List writeKtx2({
  1. required int vkFormat,
  2. required int pixelWidth,
  3. required int pixelHeight,
  4. required List<Uint8List> levels,
  5. Map<String, String> keyValues = const <String, String>{},
})

Assembles a KTX2 file from already-encoded mip levels — the writer half of ktx2_loader.dart, and the reason ap-07 in doc/asset-pipeline-plan.md lives beside it rather than in flutter3d_build: both read and write the same container, in the same package, with no Flutter SDK behind either.

No data format descriptor. The specification asks for a DFD; ktx2_loader.dart's own doc comment says why this engine's reader never looks at one — it takes the format from the header alone — so a writer whose only reader is this one has nothing to gain from spending bytes on a section nothing here parses. A file this writes and a third-party tool (RenderDoc, ktx2check) opens would want one; nothing in this repository does that yet, and adding the section later is additive, the same reasoning ktx2_format.dart gives for refusing a feature outright rather than half-writing it.

keyValues writes the section the DFD's reasoning does not cover — gfx-83n needs one entry, the key that says a file's blocks are the universal intermediate, because that file's vkFormat is undefined and the header therefore cannot say it. Entries are written in the order given.

levels is level 0 (the base, largest image) first, matching Ktx2Texture.levels' own order — a caller with an encoder's single-level output and a caller with ap-08's future mip chain call this the same way.

Implementation

Uint8List writeKtx2({
  required int vkFormat,
  required int pixelWidth,
  required int pixelHeight,
  required List<Uint8List> levels,
  Map<String, String> keyValues = const <String, String>{},
}) {
  if (levels.isEmpty) {
    throw ArgumentError('writeKtx2 needs at least one level (level 0).');
  }

  final levelCount = levels.length;
  final levelIndexEnd =
      kKtx2LevelIndexOffset + levelCount * kKtx2LevelIndexEntryBytes;

  // Each entry is a u32 length, then the key, a NUL, the value and its own
  // NUL, then padding to the next multiple of four — the layout
  // `_checkKeyValues` reads back.
  final kvdByteOffset = levelIndexEnd;
  final entries = <Uint8List>[];
  for (final MapEntry(:key, :value) in keyValues.entries) {
    final payload = <int>[...key.codeUnits, 0, ...value.codeUnits, 0];
    final padded = (payload.length + 3) & ~3;
    final entry = Uint8List(4 + padded);
    ByteData.sublistView(entry).setUint32(0, payload.length, Endian.little);
    entry.setRange(4, 4 + payload.length, payload);
    entries.add(entry);
  }
  final kvdByteLength = entries.fold(0, (sum, e) => sum + e.lengthInBytes);

  final levelOffsets = <int>[];
  var cursor = kvdByteOffset + kvdByteLength;
  for (final level in levels) {
    levelOffsets.add(cursor);
    cursor += level.lengthInBytes;
  }

  final bytes = Uint8List(cursor);
  bytes.setRange(0, kKtx2Identifier.length, kKtx2Identifier);
  final view = ByteData.sublistView(bytes);

  void putHeader(int field, int value) =>
      view.setUint32(kKtx2HeaderOffset + field, value, Endian.little);
  putHeader(Ktx2HeaderField.vkFormat, vkFormat);
  // Block-compressed formats carry no per-texel type: the spec's own
  // wording for `typeSize` is "1 for block-compressed formats".
  putHeader(Ktx2HeaderField.typeSize, 1);
  putHeader(Ktx2HeaderField.pixelWidth, pixelWidth);
  putHeader(Ktx2HeaderField.pixelHeight, pixelHeight);
  putHeader(Ktx2HeaderField.pixelDepth, 0);
  putHeader(Ktx2HeaderField.layerCount, 0);
  putHeader(Ktx2HeaderField.faceCount, 1);
  putHeader(Ktx2HeaderField.levelCount, levelCount);
  putHeader(
    Ktx2HeaderField.supercompressionScheme,
    Ktx2SupercompressionScheme.none,
  );

  if (kvdByteLength != 0) {
    view.setUint32(
      kKtx2IndexOffset + Ktx2IndexField.kvdByteOffset,
      kvdByteOffset,
      Endian.little,
    );
    view.setUint32(
      kKtx2IndexOffset + Ktx2IndexField.kvdByteLength,
      kvdByteLength,
      Endian.little,
    );
    var at = kvdByteOffset;
    for (final entry in entries) {
      bytes.setRange(at, at + entry.lengthInBytes, entry);
      at += entry.lengthInBytes;
    }
  }

  for (var i = 0; i < levelCount; i++) {
    final entry = kKtx2LevelIndexOffset + i * kKtx2LevelIndexEntryBytes;
    final length = levels[i].lengthInBytes;
    // 64-bit fields as two little-endian 32-bit halves, high half always
    // zero — the same limit `ktx2_loader.dart`'s `_readOffsetOrLength`
    // states, and nothing an encoder in this package ever produces gets
    // near it.
    view.setUint32(entry, levelOffsets[i], Endian.little);
    view.setUint32(entry + 4, 0, Endian.little);
    view.setUint32(entry + 8, length, Endian.little);
    view.setUint32(entry + 12, 0, Endian.little);
    // No supercompression, so the uncompressed length is the stored length.
    view.setUint32(entry + 16, length, Endian.little);
    view.setUint32(entry + 20, 0, Endian.little);
    bytes.setRange(levelOffsets[i], levelOffsets[i] + length, levels[i]);
  }

  return bytes;
}