Ktx2Texture.parse constructor

Ktx2Texture.parse(
  1. Uint8List bytes, {
  2. UniversalTarget? universalTarget,
})

Reads bytes as a KTX2 file.

Throws Ktx2FormatException rather than returning null: a caller that picked this decoder has already decided the bytes are a .ktx2, and a silent null would surface later as a missing texture with no reason.

universalTarget is the GPU format a universal-block file is turned into on the way past — gfx-83n. It is required for such a file and ignored for every other, because the choice is the device's and this package cannot see one: universalBlockFormat answers whether a file needs it, and the engine's own wrapper picks the target from what the device says it samples.

Implementation

factory Ktx2Texture.parse(
  Uint8List bytes, {
  UniversalTarget? universalTarget,
}) {
  if (bytes.lengthInBytes < kKtx2LevelIndexOffset) {
    throw Ktx2FormatException(
      'File is ${bytes.lengthInBytes} bytes, too short for a KTX2 header.',
    );
  }
  for (var i = 0; i < kKtx2Identifier.length; i++) {
    if (bytes[i] != kKtx2Identifier[i]) {
      throw Ktx2FormatException(
        'Not a KTX2 file: byte $i is 0x${bytes[i].toRadixString(16)}, '
        'expected 0x${kKtx2Identifier[i].toRadixString(16)}.',
      );
    }
  }

  final view = ByteData.view(
    bytes.buffer,
    bytes.offsetInBytes,
    bytes.lengthInBytes,
  );
  int header(int field) =>
      view.getUint32(kKtx2HeaderOffset + field, Endian.little);

  final vkFormat = header(Ktx2HeaderField.vkFormat);
  final pixelWidth = header(Ktx2HeaderField.pixelWidth);
  final pixelHeight = header(Ktx2HeaderField.pixelHeight);
  final pixelDepth = header(Ktx2HeaderField.pixelDepth);
  final layerCount = header(Ktx2HeaderField.layerCount);
  final faceCount = header(Ktx2HeaderField.faceCount);
  final levelCount = header(Ktx2HeaderField.levelCount);
  final supercompressionScheme = header(
    Ktx2HeaderField.supercompressionScheme,
  );

  // Shape checks that apply whichever way the pixels are stored — moved
  // ahead of the format branch below so a texture array or a cube map is
  // refused by the same message whether it is a plain format or Basis
  // Universal.
  if (pixelDepth != 0) {
    throw Ktx2FormatException(
      '3D textures (pixelDepth=$pixelDepth) are not supported yet.',
    );
  }
  if (layerCount != 0) {
    throw Ktx2FormatException(
      'Texture arrays (layerCount=$layerCount) are not supported yet.',
    );
  }
  if (faceCount != 1) {
    throw Ktx2FormatException(
      'Cube maps (faceCount=$faceCount) are not supported yet.',
    );
  }
  final keyValues = _checkKeyValues(bytes, view);

  // `vkFormat == 0` (VK_FORMAT_UNDEFINED) is how a KTX2 file says "this is
  // Basis Universal" — the real format then lives in the supercompression
  // global data below, not in this field.
  if (vkFormat == VkFormat.undefined) {
    final universal = keyValues[kUniversalBlockKey];
    if (universal != null) {
      if (levelCount == 0) {
        throw const Ktx2FormatException(
          'levelCount is 0, which asks the loader to generate mip levels at '
          'load time — not implemented yet.',
        );
      }
      if (supercompressionScheme != Ktx2SupercompressionScheme.none) {
        throw Ktx2FormatException(
          'A universal-block file is ${_supercompressionName(supercompressionScheme)}-'
          'compressed, and only an uncompressed one is read here — the '
          'transcode and the decompression would both have to run on the '
          'load, and nothing writes this combination yet.',
        );
      }
      return _parseUniversal(
        bytes,
        view,
        pixelWidth,
        pixelHeight,
        levelCount,
        universal,
        universalTarget,
      );
    }
    // **Which Basis Universal, from the data format descriptor — `gfx-78n`.**
    // An undefined `vkFormat` says "Basis" and nothing more. The
    // supercompression scheme used to stand in for the rest — Basis-LZ
    // means ETC1S, anything else must be UASTC — which was only ever a
    // guess, and the descriptor's colour model is the field that says. A
    // file with no descriptor at all still gets the old inference for
    // Basis-LZ, which cannot be anything but ETC1S: the codebooks it names
    // are ETC1S's.
    final colorModel = _colorModelOf(bytes, view);
    if (colorModel == Ktx2ColorModel.uastc) {
      return _parseUastc(
        bytes,
        view,
        pixelWidth,
        pixelHeight,
        levelCount,
        supercompressionScheme,
      );
    }
    if ((colorModel != null && colorModel != Ktx2ColorModel.etc1s) ||
        supercompressionScheme != Ktx2SupercompressionScheme.basisLZ) {
      final named = colorModel == null
          ? 'there is no descriptor'
          : 'it names colour model $colorModel '
                '(${Ktx2ColorModel.nameOf(colorModel)})';
      throw Ktx2FormatException(
        'vkFormat is undefined, so the pixels are Basis Universal and the '
        'data format descriptor says which kind: $named, under '
        'supercompression scheme $supercompressionScheme '
        '(${_supercompressionName(supercompressionScheme)}). What is read '
        'here is ETC1S under Basis-LZ, and UASTC LDR 4x4 under none, '
        'Zstandard or ZLIB.',
      );
    }
    if (levelCount == 0) {
      throw const Ktx2FormatException(
        'levelCount is 0, which asks the loader to generate mip levels at '
        'load time — not implemented yet.',
      );
    }
    return _parseBasisEtc1s(bytes, view, pixelWidth, pixelHeight, levelCount);
  }

  return Ktx2Texture._(
    pixelWidth,
    pixelHeight,
    vkFormat,
    _readLevels(bytes, view, levelCount, supercompressionScheme),
  );
}