From 7b42819b643fffd835b47b34171717104ce864e9 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 15 Jan 2025 14:55:19 +0000 Subject: [PATCH 01/12] feat: Support ktx2 compressed textures --- hxd/CompressedTextureFormat.hx | 22 + hxd/PixelFormat.hx | 6 +- hxd/Pixels.hx | 54 +- hxd/net/BinaryLoader.hx | 4 +- hxd/res/Image.hx | 23 + hxd/res/Ktx2.hx | 999 +++++++++++++++++++++++++++++++++ 6 files changed, 1098 insertions(+), 10 deletions(-) create mode 100644 hxd/CompressedTextureFormat.hx create mode 100644 hxd/res/Ktx2.hx diff --git a/hxd/CompressedTextureFormat.hx b/hxd/CompressedTextureFormat.hx new file mode 100644 index 0000000000..656309d76c --- /dev/null +++ b/hxd/CompressedTextureFormat.hx @@ -0,0 +1,22 @@ +package hxd; + +enum abstract ASTC_FORMAT(Int) from Int to Int { + final RGBA_4x4 = 0x93B0; +} + +enum abstract DXT_FORMAT(Int) from Int to Int { + final RGB_DXT1 = 0x83F0; + final RGBA_DXT1 = 0x83F1; + final RGBA_DXT3 = 0x83F2; + final RGBA_DXT5 = 0x83F3; +} + +enum abstract ETC_FORMAT(Int) from Int to Int { + final RGB_ETC1 = 0x8D64; + final RGBA_ETC2 = 0x9278; +} + +enum abstract BPTC_FORMAT(Int) from Int to Int { + final RGB_BPTC_UNSIGNED = 0x8E8F; + final RGBA_BPTC = 0x8E8C; +} diff --git a/hxd/PixelFormat.hx b/hxd/PixelFormat.hx index 6ad043c686..ebbf9438c4 100644 --- a/hxd/PixelFormat.hx +++ b/hxd/PixelFormat.hx @@ -23,9 +23,11 @@ enum PixelFormat { RG16U; RGB16U; RGBA16U; - S3TC( v : Int ); + ASTC(v:Int); + ETC(v:Int); + S3TC(v:Int); Depth16; Depth24; Depth24Stencil8; Depth32; -} \ No newline at end of file +} diff --git a/hxd/Pixels.hx b/hxd/Pixels.hx index b56a0f75da..dc61fe4cd4 100644 --- a/hxd/Pixels.hx +++ b/hxd/Pixels.hx @@ -391,6 +391,8 @@ class Pixels { this.bytes = nbytes; case [S3TC(a),S3TC(b)] if( a == b ): + case [ASTC(a),ASTC(b)] if( a == b ): + case [ETC(a),ETC(b)] if( a == b ): // nothing #if (hl && hl_ver >= "1.10") @@ -536,8 +538,33 @@ class Pixels { public static function calcDataSize( width : Int, height : Int, format : PixelFormat ) { return switch( format ) { - case S3TC(_): - (((height + 3) >> 2) << 2) * calcStride(width, format); + case S3TC(n): + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + var blocks = w * h; // Total number of blocks + if( n == 3 ) { // DXT5 + blocks * 16; // 16 bytes per block + } else if( n == 1 || n == 4 ) { + blocks * 8; // DXT1 or BC4, 8 bytes per block + } else { + blocks * 16; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity + } + case ASTC(n): + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + w * h * 16; + case ETC(n): + if( n == 0 ) { // RGB_ETC1_Format or RGB_ETC2_Format + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + w * h * 8; + } else if( n == 1 || n == 2 ) { // RGBA_ETC2_EAC_Format + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + w * h * 16; + } else { + throw "Unsupported ETC format"; + } default: height * calcStride(width, format); } @@ -559,11 +586,26 @@ class Pixels { case RGB32F: 12; case RGB10A2: 4; case RG11B10UF: 4; + case ASTC(n): + var blocks = ((width + 3) >> 2) * 16; + blocks << 4; + case ETC(n): + if( n == 0 ) { // ETC1 and ETC2 RGB + ((width + 3) >> 2) << 3; + } else if( n == 1 ) { // ETC2 EAC RGBA + ((width + 3) >> 2) << 4; + } else { + throw "Unsupported ETC format"; + } case S3TC(n): var blocks = (width + 3) >> 2; - if( n == 1 || n == 4 ) - return blocks << 1; - return blocks << 2; + if( n == 3 ) { // DXT5 + blocks << 4; // 16 bytes per block + } else if( n == 1 || n == 4 ) { + blocks << 1; // DXT1 or BC4, 8 bytes per block + } else { + blocks << 2; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity + } case Depth16: 2; case Depth24: 3; case Depth24Stencil8, Depth32: 4; @@ -605,7 +647,7 @@ class Pixels { channel.toInt() * 4; case RGB10A2, RG11B10UF: throw "Bit packed format"; - case S3TC(_), Depth16, Depth24, Depth24Stencil8, Depth32: + case S3TC(_), ASTC(_), ETC(_), Depth16, Depth24, Depth24Stencil8, Depth32: throw "Not supported"; } } diff --git a/hxd/net/BinaryLoader.hx b/hxd/net/BinaryLoader.hx index d10a0c37fa..24bb581172 100644 --- a/hxd/net/BinaryLoader.hx +++ b/hxd/net/BinaryLoader.hx @@ -18,7 +18,7 @@ class BinaryLoader { throw msg; } - public function load() { + public function load(raw = false) { #if js var xhr = new js.html.XMLHttpRequest(); @@ -32,7 +32,7 @@ class BinaryLoader { onError(xhr.statusText); return; } - onLoaded(haxe.io.Bytes.ofData(xhr.response)); + onLoaded(raw ? xhr.response : haxe.io.Bytes.ofData(xhr.response)); } xhr.onprogress = function(e) { diff --git a/hxd/res/Image.hx b/hxd/res/Image.hx index 76a3f7206d..75c04190eb 100644 --- a/hxd/res/Image.hx +++ b/hxd/res/Image.hx @@ -8,6 +8,7 @@ enum abstract ImageFormat(Int) { var Dds = 4; var Raw = 5; var Hdr = 6; + var Ktx2 = 7; /* Tells if we might not be able to directly decode the image without going through a loadBitmap async call. @@ -35,6 +36,7 @@ enum abstract ImageFormat(Int) { case Dds: "DDS"; case Raw: "RAW"; case Hdr: "HDR"; + case Ktx2: "KTX2"; }; } } @@ -242,6 +244,22 @@ class Image extends Resource { throw entry.path + " has unsupported 4CC " + fid; } + #if js + case 0x4273: + throw 'Use .ktx2 files for GPU compressed textures instead of .basis'; + case 0x4BAB: + final ktx2 = hxd.res.Ktx2.readFile(new haxe.io.BytesInput(@:privateAccess f.cache)); + inf.pixelFormat = switch ktx2.dfd.colorModel { + case hxd.res.Ktx2.DFDModel.ETC1S: ETC(hxd.res.Ktx2.TranscoderFormat.ETC1); + case hxd.res.Ktx2.DFDModel.UASTC: ASTC(hxd.res.Ktx2.TranscoderFormat.ASTC_4x4); + default: throw 'Unsupported colorModel in ktx2 file ${ktx2.dfd.colorModel}'; + } + inf.mipLevels = ktx2.header.levelCount; + inf.width = ktx2.header.pixelWidth; + inf.height = ktx2.header.pixelHeight; + inf.dataFormat = Ktx2; + #end + case 0x3F23: // HDR RADIANCE inf.dataFormat = Hdr; @@ -435,6 +453,9 @@ class Image extends Resource { case Hdr: var data = hxd.fmt.hdr.Reader.decode(entry.getBytes(), false); pixels = new hxd.Pixels(data.width, data.height, data.bytes, inf.pixelFormat); + case Ktx2: + var bytes = entry.getBytes(); + pixels = new hxd.Pixels(inf.width, inf.height, bytes, inf.pixelFormat); } if (fmt != null) pixels.convert(fmt); @@ -610,6 +631,8 @@ class Image extends Resource { pos += size; } } + case Ktx2: + throw 'Ktx2 loading using heaps resource system not implemented'; default: for (layer in 0...tex.layerCount) { for (mip in 0...inf.mipLevels) { diff --git a/hxd/res/Ktx2.hx b/hxd/res/Ktx2.hx new file mode 100644 index 0000000000..d67fb9fb56 --- /dev/null +++ b/hxd/res/Ktx2.hx @@ -0,0 +1,999 @@ +package hxd.res; +#if js +import haxe.io.UInt8Array; +using Lambda; +/** + Ktx2 file parser. +**/ +class Ktx2 { + static inline final BYTE_INDEX_ERROR = 'ktx2 files with a file size exceeding 32 bit address space is not supported'; + + /** + Read ktx2 file + + @param bytes BytesInput containing ktx2 file data + + @return Parsed ktx2 file + **/ + public static function readFile( bytes : haxe.io.BytesInput ) : Ktx2File { + final header = readHeader(bytes); + final levels = readLevels(bytes, header.levelCount); + final dfd = readDfd(bytes); + final file : Ktx2File = { + header: header, + levels: levels, + dfd: dfd, + data: new js.lib.Uint8Array(@:privateAccess bytes.b), + supercompressionGlobalData: null, + } + return file; + } + + public static function readHeader( bytes : haxe.io.BytesInput) : KTX2Header { + final ktx2Id = [ + // '´', 'K', 'T', 'X', '2', '0', 'ª', '\r', '\n', '\x1A', '\n' + 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A, + ]; + + final matching = ktx2Id.mapi((i, id) -> id == bytes.readByte()); + + if( matching.contains(false) ) { + throw 'Invalid KTX2 header'; + } + final header :KTX2Header = { + vkFormat: bytes.readInt32(), + typeSize: bytes.readInt32(), + pixelWidth: bytes.readInt32(), + pixelHeight: bytes.readInt32(), + pixelDepth: bytes.readInt32(), + layerCount: bytes.readInt32(), + faceCount: bytes.readInt32(), + levelCount: bytes.readInt32(), + supercompressionScheme: bytes.readInt32(), + + dfdByteOffset: bytes.readInt32(), + dfdByteLength: bytes.readInt32(), + kvdByteOffset: bytes.readInt32(), + kvdByteLength: bytes.readInt32(), + sgdByteOffset: { + final val = bytes.read(8).getInt64(0); + if( val.high>0 ) { + throw BYTE_INDEX_ERROR; + } + val.low; + }, + sgdByteLength: { + final val = bytes.read(8).getInt64(0); + if( val.high>0 ) { + throw BYTE_INDEX_ERROR; + } + val.low; + } + } + + if( header.pixelDepth>0 ) { + throw 'Failed to parse KTX2 file - Only 2D textures are currently supported.'; + } + if( header.layerCount>1 ) { + throw 'Failed to parse KTX2 file - Array textures are not currently supported.'; + } + if( header.faceCount>1 ) { + throw 'Failed to parse KTX2 file - Cube textures are not currently supported.'; + } + return header; + } + + static function readLevels( bytes : haxe.io.BytesInput, levelCount : Int) : Array { + levelCount = hxd.Math.imax(1, levelCount); + final length = levelCount * 3 * (2 * 4); + final level = bytes.read(length); + final levels:Array = []; + + while( levelCount-- > 0 ) { + levels.push({ + byteOffset: { + final val = level.getInt64(0); + if( val.high>0 ) { + throw BYTE_INDEX_ERROR; + } + val.low; + }, + byteLength: { + final val = level.getInt64(8); + if( val.high>0 ) { + throw BYTE_INDEX_ERROR; + } + val.low; + }, + uncompressedByteLength: { + final val = level.getInt64(16); + if( val.high>0 ) { + throw BYTE_INDEX_ERROR; + } + val.low; + }, + }); + } + return levels; + } + + static function readDfd( bytes : haxe.io.BytesInput) : KTX2DFD { + final totalSize = bytes.readInt32(); + final vendorId = bytes.readInt16(); + final descriptorType = bytes.readInt16(); + final versionNumber = bytes.readInt16(); + final descriptorBlockSize = bytes.readInt16(); + final numSamples = Std.int((descriptorBlockSize-24)/16); + final dfdBlock : KTX2DFD = { + vendorId:vendorId, + descriptorType: descriptorType, + versionNumber: versionNumber, + descriptorBlockSize: descriptorBlockSize, + colorModel: bytes.readByte(), + colorPrimaries: bytes.readByte(), + transferFunction: bytes.readByte(), + flags: bytes.readByte(), + texelBlockDimension: { + x: bytes.readByte()+1, + y: bytes.readByte()+1, + z: bytes.readByte()+1, + w: bytes.readByte()+1, + }, + bytesPlane: [ + bytes.readByte() /* bytesPlane0 */, + bytes.readByte() /* bytesPlane1 */, + bytes.readByte() /* bytesPlane2 */, + bytes.readByte() /* bytesPlane3 */, + bytes.readByte() /* bytesPlane4 */, + bytes.readByte() /* bytesPlane5 */, + bytes.readByte() /* bytesPlane6 */, + bytes.readByte() /* bytesPlane7 */, + ], + numSamples: numSamples, + samples: [ + for( i in 0...numSamples ) { + final bitOffset = bytes.readUInt16(); + final bitLength = bytes.readByte()+1; + final channelType = bytes.readByte(); + final channelFlags = (channelType & 0xf0)>>4; + final samplePosition = [ + bytes.readByte() /* samplePosition0 */, + bytes.readByte() /* samplePosition1 */, + bytes.readByte() /* samplePosition2 */, + bytes.readByte() /* samplePosition3 */, + ]; + final sampleLower = bytes.readUInt16()+bytes.readUInt16(); + final sampleUpper = bytes.readUInt16()+bytes.readUInt16(); + final sample : KTX2Sample = { + bitOffset: bitOffset, + bitLength: bitLength, + channelType: channelType & 0x0F, + channelFlags: channelFlags, + samplePosition: samplePosition, + sampleLower: sampleLower, + sampleUpper: sampleUpper, + }; + sample; + } + ], + } + return dfdBlock; + } +} + +class Ktx2Decoder { + public static var mscTranscoder : Dynamic; + public static var workerLimit = 4; + + static var _workerNextTaskID = 1; + static var _workerSourceURL : String; + static var _workerConfig : BasisWorkerConfig; + static var _workerPool : Array = []; + static var _transcoderPending : js.lib.Promise; + static var _transcoderBinary : haxe.io.Bytes; + static var _transcoderScript : String; + static var _transcoderLoading : js.lib.Promise<{ script : String, wasm : haxe.io.Bytes }>; + + public static function getTexture(bytes : haxe.io.BytesInput, cb : (texture : h3d.mat.Texture) -> Void) { + createTexture(bytes, cb); + } + + static function detectSupport(fmt : KtxTranscodeTarget) { + final driver : h3d.impl.GlDriver = cast h3d.Engine.getCurrent().driver; + return { + astcSupported: driver.textureSupport.astc, + etc1Supported: driver.textureSupport.etc1, + etc2Supported: driver.textureSupport.etc2, + dxtSupported: driver.textureSupport.dxt, + bptcSupported: driver.textureSupport.bptc, + } + } + + static function getWorker() { + return initTranscoder().then(val -> { + if( _workerPool.length < workerLimit ) { + final worker = new js.html.Worker(_workerSourceURL); + final workerTask : WorkerTask = { + worker: worker, + callbacks: new haxe.ds.IntMap(), + taskCosts: new haxe.ds.IntMap(), + taskLoad: 0, + } + worker.postMessage({ + type: 'init', + config: _workerConfig, + transcoderBinary: _transcoderBinary, + }); + + worker.onmessage = e -> { + var message = e.data; + switch( message.type ) { + case 'transcode': + workerTask.callbacks.get(message.id).resolve(message); + case 'error': + workerTask.callbacks.get(message.id).reject(message); + default: + throw 'Ktx2Loader: Unexpected message, "${message.type}"'; + } + }; + _workerPool.push(workerTask); + } else { + _workerPool.sort((a, b) -> a.taskLoad > b.taskLoad ? -1 : 1); + } + + return _workerPool[_workerPool.length-1]; + }); + } + + static function createTexture( buffer : haxe.io.BytesInput, cb : (texture:h3d.mat.Texture) -> Void) { + final ktx = Ktx2.readFile(buffer); + + final w = ktx.header.pixelWidth; + final h = ktx.header.pixelHeight; + + final transcodeTarget = switch ktx.dfd.colorModel { + case hxd.res.Ktx2.DFDModel.ETC1S: + KtxTranscodeTarget.ETC1S({}, { + fmt: CompressedFormat.ETC1, + alpha: ktx.dfd.hasAlpha(), + needsPowerOfTwo: true, + }); + case hxd.res.Ktx2.DFDModel.UASTC: + KtxTranscodeTarget.UASTC({}, { + fmt: CompressedFormat.ASTC, + alpha: ktx.dfd.hasAlpha(), + needsPowerOfTwo: true, + }); + default: throw 'Unsupported colorModel in ktx2 file ${ktx.dfd.colorModel}'; + } + _workerConfig = detectSupport(transcodeTarget); + getWorker().then(task -> { + final worker = task.worker; + final taskID = _workerNextTaskID++; + + final textureDone = new js.lib.Promise((resolve, reject) -> { + task.callbacks.set(taskID, { + resolve: resolve, + reject: reject, + }); + task.taskCosts.set(taskID, buffer.length); + task.taskLoad += task.taskCosts.get(taskID); + buffer.position = 0; + final bytes = buffer.readAll().getData(); + worker.postMessage({type: 'transcode', id: taskID, buffer: bytes}, [bytes]); + }); + + textureDone.then(( message : BasisWorkerMessage )-> { + if( message.type == 'error' ) { + throw 'Unable to decode ktx2 file: ${message.error}'; + } + + final w = message.data.width; + final h = message.data.height; + final create = (fmt:hxd.PixelFormat) -> { + if( ktx.header.faceCount > 1 || ktx.header.layerCount > 1 ) { + // TODO: Handle cube texture + throw 'Multi texture ktx2 files not supported'; + } + final face = message.data.faces[0]; + final mipmaps:Array = face.mipmaps; + final texture = new h3d.mat.Texture(w, h, null, fmt); + var level = 0; + for (mipmap in mipmaps) { + final bytes = haxe.io.Bytes.ofData(cast mipmap.data); + final pixels = new hxd.Pixels(mipmap.width, mipmap.height, bytes, fmt); + texture.uploadPixels(pixels, level); + level++; + } + if(mipmaps.length>1) { + texture.flags.set(MipMapped); + texture.mipMap = Linear; + } + texture; + } + final texture = switch message.data.format { + case EngineFormat.RGBA_ASTC_4x4_Format: + create(hxd.PixelFormat.ASTC(10)); + case EngineFormat.RGB_BPTC_UNSIGNED_Format: + create(hxd.PixelFormat.S3TC(6)); + case EngineFormat.RGBA_BPTC_Format: + create(hxd.PixelFormat.S3TC(7)); + case EngineFormat.RGBA_S3TC_DXT5_Format: + create(hxd.PixelFormat.S3TC(3)); + case EngineFormat.RGB_ETC1_Format: + create(hxd.PixelFormat.ETC(0)); + case EngineFormat.RGBA_ETC2_EAC_Format: + create(hxd.PixelFormat.ETC(1)); + default: + throw 'Ktx2Loader: No supported format available.'; + } + + if( task != null && taskID > 0 ) { + task.taskLoad -= task.taskCosts.get(taskID); + task.callbacks.remove(taskID); + task.taskCosts.remove(taskID); + } + cb(texture); + }); + }); + + } + + static function initTranscoder() { + _transcoderLoading = if( _transcoderLoading == null ) { + // Load transcoder wrapper. + final jsLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.js'); + final jsContent = new js.lib.Promise((resolve, reject) -> { + jsLoader.onLoaded = resolve; + jsLoader.onError = reject; + jsLoader.load(); + }); + // Load transcoder WASM binary. + final binaryLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.wasm'); + final binaryContent = new js.lib.Promise((resolve, reject) -> { + binaryLoader.onLoaded = resolve; + binaryLoader.onError = reject; + binaryLoader.load(true); + }); + js.lib.Promise.all([jsContent, binaryContent]).then(arr -> {script:arr[0].toString(), wasm:arr[1]}); + } else { + _transcoderLoading; + } + + _transcoderPending = _transcoderLoading.then(o -> { + _transcoderScript = o.script; + _transcoderBinary = o.wasm; + final transcoderFormat = Type.getClassFields(TranscoderFormat).map(f -> '"$f": ${Reflect.field(TranscoderFormat, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; + final basisFormat = Type.allEnums(BasisFormat).fold((curr, acc) -> '$acc\t"${curr.getName()}": ${curr.getIndex()},\n', '{\n') + '}'; + final engineFormat = Type.getClassFields(EngineFormat).map(f -> '"$f": ${Reflect.field(EngineFormat, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; + final engineType = Type.getClassFields(EngineType).map(f -> '"$f": ${Reflect.field(EngineType, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; + final body = [ + '/* constants */', + 'let _EngineFormat = $engineFormat', + 'let _EngineType = $engineType', + 'let _TranscoderFormat = $transcoderFormat', + 'let _BasisFormat = $basisFormat', + '/* basis_transcoder.js */', + _transcoderScript, + '/* worker */', + basisWorker() + ].join('\n'); + + _workerSourceURL = js.html.URL.createObjectURL(new js.html.Blob([body])); + }); + return _transcoderPending; + } +} + +typedef Ktx2File = { + header : KTX2Header, + levels : Array, + dfd : KTX2DFD, + data : js.lib.Uint8Array, + supercompressionGlobalData : KTX2SupercompressionGlobalData, +} + +enum abstract SuperCompressionScheme(Int) from Int to Int { + final NONE = 0; + final BASISLZ = 1; + final ZSTANDARD = 2; + final ZLIB = 3; +} + +enum abstract DFDModel(Int) from Int to Int { + final ETC1S = 163; + final UASTC = 166; +} + +enum abstract DFDChannel_ETC1S(Int) from Int to Int { + final RGB = 0; + final RRR = 3; + final GGG = 4; + final AAA = 15; +} + +enum abstract DFDChannel_UASTC(Int) from Int to Int { + final RGB = 0; + final RGBA = 3; + final RRR = 4; + final RRRG = 5; +} + +enum abstract DFDTransferFunction(Int) from Int to Int { + final LINEAR = 1; + final SRGB = 2; +} + +enum abstract SupercompressionScheme(Int) from Int to Int { + public final None = 0; + public final BasisLZ = 1; + public final ZStandard = 2; + public final ZLib = 3; +} + +/** @internal */ +@:structInit class KTX2Header { + public final vkFormat : Int; + public final typeSize : Int; + public final pixelWidth : Int; + public final pixelHeight : Int; + public final pixelDepth : Int; + public final layerCount : Int; + public final faceCount : Int; + public final levelCount : Int; + public final supercompressionScheme : Int; + public final dfdByteOffset : Int; + public final dfdByteLength : Int; + public final kvdByteOffset : Int; + public final kvdByteLength : Int; + public final sgdByteOffset : Int; + public final sgdByteLength : Int; + + public function needZSTDDecoder() { + return supercompressionScheme == SupercompressionScheme.ZStandard; + } +} + +/** @internal */ +typedef KTX2Level = { + /** + Byte offset. According to spec this should be 64 bit, but since a lot of byte code in haxe is using regular 32 bit Int for indexing, + supporting files to large to fit in 32bit space is complicated and should not be needed for individual game assets. + **/ + final byteOffset : Int; + final byteLength : Int; + final uncompressedByteLength : Int; +} + +typedef KTX2Sample = { + final bitOffset : Int; + final bitLength : Int; + final channelType : Int; + final channelFlags : Int; + final samplePosition : Array; + final sampleLower : Int; + final sampleUpper : Int; +} + +/** @internal */ +@:structInit class KTX2DFD { + public final vendorId : Int; + public final descriptorType : Int; + public final versionNumber : Int; + public final descriptorBlockSize : Int; + public final colorModel : Int; + public final colorPrimaries : Int; + public final transferFunction : Int; + public final flags : Int; + public final texelBlockDimension : { + x : Int, + y : Int, + z : Int, + w : Int, + }; + public final bytesPlane : Array; + public final numSamples : Int; + public final samples : Array; + + public function hasAlpha() { + return switch colorModel { + case hxd.res.Ktx2.DFDModel.ETC1S: + numSamples == 2 && (samples[0].channelType == DFDChannel_ETC1S.AAA || samples[1].channelType == DFDChannel_ETC1S.AAA); + case hxd.res.Ktx2.DFDModel.UASTC: + samples[0].channelType == DFDChannel_UASTC.RGBA; + default: throw 'Unsupported colorModel in ktx2 file ${colorModel}'; + } + } + + public function isInGammaSpace() { + return transferFunction == DFDTransferFunction.SRGB; + } +} + +/** @internal */ +typedef KTX2ImageDesc = { + final imageFlags : Int; + final rgbSliceByteOffset : Int; + final rgbSliceByteLength : Int; + final alphaSliceByteOffset : Int; + final alphaSliceByteLength : Int; +} + +/** @internal */ +typedef KTX2SupercompressionGlobalData = { + final endpointCount : Int; + final selectorCount : Int; + final endpointsByteLength : Int; + final selectorsByteLength : Int; + final tablesByteLength : Int; + final extendedByteLength : Int; + final imageDescs : Array; + final endpointsData : haxe.io.UInt8Array; + final selectorsData : haxe.io.UInt8Array; + final tablesData : haxe.io.UInt8Array; + final extendedData : haxe.io.UInt8Array; +} + +@:keep +class TranscoderFormat { + public static final ETC1 = 0; + public static final ETC2 = 1; + public static final BC1 = 2; + public static final BC3 = 3; + public static final BC4 = 4; + public static final BC5 = 5; + public static final BC7_M6_OPAQUE_ONLY = 6; + public static final BC7_M5 = 7; + + public static final ASTC_4x4 = 10; + public static final ATC_RGB = 11; + public static final ATC_RGBA_INTERPOLATED_ALPHA = 12; + public static final RGBA32 = 13; + public static final RGB565 = 14; + public static final BGR565 = 15; + public static final RGBA4444 = 16; + public static final BC6H = 22; + public static final RGB_HALF = 24; + public static final RGBA_HALF = 25; +} + +enum TranscoderType { + cTFETC1; + cTFETC2; // Not used + cTFBC1; + cTFBC3; + cTFBC4; // Not used + cTFBC5; // Not used + cTFBC7_M6_OPAQUE_ONLY; // Not used + cTFBC7_M5; // Not used + cTFPVRTC1_4_RGB; // Not used + cTFPVRTC1_4_RGBA; // Not used + cTFASTC_4x4; + cTFATC_RGB1; // Not used + cTFATC_RGBA_INTERPOLATED_ALPHA2; // Not used + cTFRGBA321; + cTFRGB5654; // Not used + cTFBGR5655; // Not used + cTFRGBA44446; // Not used +} + +@:keep +enum BasisFormat { + ETC1S; + UASTC; + UASTC_HDR; +} + +@:keep +class EngineFormat { + public static final RGBAFormat = 0x03FF; + public static final RGBA8Format = 0x8058; + public static final R8Format = 0x8229; + public static final RG8Format = 0x822b; + public static final RGBA_ASTC_4x4_Format = CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4; + public static final RGB_BPTC_UNSIGNED_Format = CompressedTextureFormat.BPTC_FORMAT.RGB_BPTC_UNSIGNED; + public static final RGBA_BPTC_Format = CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC; + public static final RGB_S3TC_DXT1_Format = CompressedTextureFormat.DXT_FORMAT.RGB_DXT1; + public static final RGBA_S3TC_DXT1_Format = CompressedTextureFormat.DXT_FORMAT.RGBA_DXT1; + public static final RGBA_S3TC_DXT5_Format = CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5; + public static final RGB_ETC1_Format = CompressedTextureFormat.ETC_FORMAT.RGB_ETC1; + public static final RGBA_ETC2_EAC_Format = CompressedTextureFormat.ETC_FORMAT.RGBA_ETC2; + public static final RGB_ETC2_Format = 0x9274; +} + +@:keep +class EngineType { + public static final UnsignedByteType = 1009; + public static final FloatType = 1015; + public static final HalfFloatType = 1016; +} + +/** + * Defines a mipmap level + */ +@:structInit class MipmapLevel { + /** + * The data of the mipmap level + */ + public var data : Null = null; + + /** + * The width of the mipmap level + */ + public final width : Int; + + /** + * The height of the mipmap level + */ + public final height : Int; +} + +enum KtxTranscodeTarget { + ETC1S(options : ETC1SDecoderOptions, caps : Ktx2Caps); + UASTC(options : UASTCDecoderOptions, caps : Ktx2Caps); +} + +@:structInit class KtxTranscodeConfig { + public final transcodeFormat :TranscodeTarget; + public final engineFormat :Int; + public final engineType = EngineType.UnsignedByteType; + public final roundToMultiple4 = true; +} + + +@:structInit class Ktx2Caps { + public final fmt : CompressedFormat; + + public final alpha : Null = null; + + public final needsPowerOfTwo = true; +} + +enum CompressedFormat { + ETC2; + ETC1; + S3TC; + ASTC; + BPTC; +} + +/** +* Options passed to the KTX2 decode function +*/ +@:structInit class UASTCDecoderOptions { + /** use RGBA format if ASTC and BC7 are not available as transcoded format */ + public final useRGBAIfASTCBC7NotAvailableWhenUASTC = false; + + /** force to always use (uncompressed) RGBA for transcoded format */ + public final forceRGBA = false; + + /** force to always use (uncompressed) R8 for transcoded format */ + public final forceR8 = false; + + /** force to always use (uncompressed) RG8 for transcoded format */ + public final forceRG8 = false; +} + +@:structInit class ETC1SDecoderOptions { + public final forceRGBA = false; +} + +enum TranscodeTarget { + ASTC_4X4_RGBA; + BC7_RGBA; + BC3_RGBA; + BC1_RGB; + ETC2_RGBA; + ETC1_RGB; + RGBA32; + R8; + RG8; +} + +typedef WorkerTask = { + worker : js.html.Worker, + callbacks : haxe.ds.IntMap<{ resolve : (value : Dynamic) -> Void, reject : (reason : Dynamic) -> Void }>, + taskCosts : haxe.ds.IntMap, + taskLoad : Int, +} + +typedef BasisWorkerConfig = { + astcSupported : Bool, + etc1Supported : Bool, + etc2Supported : Bool, + dxtSupported : Bool, +} + +function basisWorker() { + return " + let config; + let transcoderPending; + let BasisModule; + + const EngineFormat = _EngineFormat; + const EngineType = _EngineType; + const TranscoderFormat = _TranscoderFormat; + const BasisFormat = _BasisFormat; + + self.addEventListener( 'message', function ( e ) { + const message = e.data; + switch ( message.type ) { + case 'init': + config = message.config; + init( message.transcoderBinary ); + break; + case 'transcode': + transcoderPending.then( () => { + try { + const { faces, buffers, width, height, hasAlpha, format, type, dfdFlags } = transcode( message.buffer ); + self.postMessage( { type: 'transcode', id: message.id, data: { faces, width, height, hasAlpha, format, type, dfdFlags } }, buffers ); + } catch ( error ) { + console.error( error ); + self.postMessage( { type: 'error', id: message.id, error: error.message } ); + } + } ); + break; + } + } ); + + function init( wasmBinary ) { + transcoderPending = new Promise( ( resolve ) => { + BasisModule = { wasmBinary, onRuntimeInitialized: resolve }; + BASIS( BasisModule ); // eslint-disable-line no-undef + } ).then( () => { + BasisModule.initializeBasis(); + if ( BasisModule.KTX2File === undefined ) { + console.warn( 'KTX2Loader: Please update Basis Universal transcoder.' ); + } + } ); + } + + function transcode( buffer ) { + const ktx2File = new BasisModule.KTX2File( new Uint8Array( buffer ) ); + function cleanup() { + ktx2File.close(); + ktx2File.delete(); + } + + if ( ! ktx2File.isValid() ) { + cleanup(); + throw new Error( 'KTX2Loader: Invalid or unsupported .ktx2 file' ); + } + let basisFormat; + if ( ktx2File.isUASTC() ) { + basisFormat = BasisFormat.UASTC; + } else if ( ktx2File.isETC1S() ) { + basisFormat = BasisFormat.ETC1S; + } else if ( ktx2File.isHDR() ) { + basisFormat = BasisFormat.UASTC_HDR; + } else { + throw new Error( 'KTX2Loader: Unknown Basis encoding' ); + } + const width = ktx2File.getWidth(); + const height = ktx2File.getHeight(); + const layerCount = ktx2File.getLayers() || 1; + const levelCount = ktx2File.getLevels(); + const faceCount = ktx2File.getFaces(); + const hasAlpha = ktx2File.getHasAlpha(); + const dfdFlags = ktx2File.getDFDFlags(); + const { transcoderFormat, engineFormat, engineType } = getTranscoderFormat( basisFormat, width, height, hasAlpha ); + if ( ! width || ! height || ! levelCount ) { + cleanup(); + throw new Error( `KTX2Loader: Invalid texture ktx2File:${JSON.stringify(ktx2File)} w:${width} h: ${height} levelCount:${levelCount}` ); + } + + if ( ! ktx2File.startTranscoding() ) { + cleanup(); + throw new Error( 'KTX2Loader: .startTranscoding failed' ); + } + + const faces = []; + const buffers = []; + + for ( let face = 0; face < faceCount; face ++ ) { + const mipmaps = []; + for ( let mip = 0; mip < levelCount; mip ++ ) { + const layerMips = []; + let mipWidth, mipHeight; + for ( let layer = 0; layer < layerCount; layer ++ ) { + const levelInfo = ktx2File.getImageLevelInfo( mip, layer, face ); + if ( face === 0 && mip === 0 && layer === 0 && ( levelInfo.origWidth % 4 !== 0 || levelInfo.origHeight % 4 !== 0 ) ) { + console.warn( 'KTX2Loader: ETC1S and UASTC textures should use multiple-of-four dimensions.' ); + } + + if ( levelCount > 1 ) { + mipWidth = levelInfo.origWidth; + mipHeight = levelInfo.origHeight; + } else { + // Handles non-multiple-of-four dimensions in textures without mipmaps. Textures with + // mipmaps must use multiple-of-four dimensions, for some texture formats and APIs. + // See mrdoob/three.js#25908. + mipWidth = levelInfo.width; + mipHeight = levelInfo.height; + } + + let dst = new Uint8Array( ktx2File.getImageTranscodedSizeInBytes( mip, layer, 0, transcoderFormat ) ); + const status = ktx2File.transcodeImage( dst, mip, layer, face, transcoderFormat, 0, - 1, - 1 ); + + if ( engineType === EngineType.HalfFloatType ) { + dst = new Uint16Array( dst.buffer, dst.byteOffset, dst.byteLength / Uint16Array.BYTES_PER_ELEMENT ); + } + + if ( ! status ) { + cleanup(); + throw new Error( 'KTX2Loader: .transcodeImage failed.' ); + } + layerMips.push( dst ); + } + const mipData = concat( layerMips ); + mipmaps.push( { data: mipData, width: mipWidth, height: mipHeight } ); + buffers.push( mipData.buffer ); + } + faces.push( { mipmaps, width, height, format: engineFormat, type: engineType } ); + } + cleanup(); + return { faces, buffers, width, height, hasAlpha, dfdFlags, format: engineFormat, type: engineType }; + } + // + + // Optimal choice of a transcoder target format depends on the Basis format (ETC1S, UASTC, or + // UASTC HDR), device capabilities, and texture dimensions. The list below ranks the formats + // separately for each format. Currently, priority is assigned based on: + // + // high quality > low quality > uncompressed + // + // Prioritization may be revisited, or exposed for configuration, in the future. + // + // Reference: https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXDeveloperGuide.md + const FORMAT_OPTIONS = [ + { + if: 'astcSupported', + basisFormat: [ BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4 ], + engineFormat: [ EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format ], + engineType: [ EngineType.UnsignedByteType ], + priorityETC1S: Infinity, + priorityUASTC: 1, + needsPowerOfTwo: false, + }, + { + if: 'bptcSupported', + basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5 ], + engineFormat: [ EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format ], + engineType: [ EngineType.UnsignedByteType ], + priorityETC1S: 3, + priorityUASTC: 2, + needsPowerOfTwo: false, + }, + { + if: 'dxtSupported', + basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.BC1, TranscoderFormat.BC3 ], + engineFormat: [ EngineFormat.RGBA_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format ], + engineType: [ EngineType.UnsignedByteType ], + priorityETC1S: 4, + priorityUASTC: 5, + needsPowerOfTwo: false, + }, + { + if: 'etc2Supported', + basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.ETC1, TranscoderFormat.ETC2 ], + engineFormat: [ EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format ], + engineType: [ EngineType.UnsignedByteType ], + priorityETC1S: 1, + priorityUASTC: 3, + needsPowerOfTwo: false, + }, + { + if: 'etc1Supported', + basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.ETC1 ], + engineFormat: [ EngineFormat.RGB_ETC1_Format ], + engineType: [ EngineType.UnsignedByteType ], + priorityETC1S: 2, + priorityUASTC: 4, + needsPowerOfTwo: false, + }, + + // Uncompressed fallbacks. + + { + basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], + transcoderFormat: [ TranscoderFormat.RGBA32, TranscoderFormat.RGBA32 ], + engineFormat: [ EngineFormat.RGBAFormat, EngineFormat.RGBAFormat ], + engineType: [ EngineType.UnsignedByteType, EngineType.UnsignedByteType ], + priorityETC1S: 100, + priorityUASTC: 100, + needsPowerOfTwo: false, + }, + { + basisFormat: [ BasisFormat.UASTC_HDR ], + transcoderFormat: [ TranscoderFormat.RGBA_HALF ], + engineFormat: [ EngineFormat.RGBAFormat ], + engineType: [ EngineType.HalfFloatType ], + priorityHDR: 100, + needsPowerOfTwo: false, + } + ]; + + const OPTIONS = { + // TODO: For ETC1S we intentionally sort by _UASTC_ priority, preserving + // a historical accident shown to avoid performance pitfalls for Linux with + // Firefox & AMD GPU (RadeonSI). Further work needed. + // See https://github.com/mrdoob/three.js/pull/29730. + [ BasisFormat.ETC1S ]: FORMAT_OPTIONS + .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.ETC1S ) ) + .sort( ( a, b ) => a.priorityUASTC - b.priorityUASTC ), + + [ BasisFormat.UASTC ]: FORMAT_OPTIONS + .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.UASTC ) ) + .sort( ( a, b ) => a.priorityUASTC - b.priorityUASTC ), + + [ BasisFormat.UASTC_HDR ]: FORMAT_OPTIONS + .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.UASTC_HDR ) ) + .sort( ( a, b ) => a.priorityHDR - b.priorityHDR ), + }; + + function getTranscoderFormat( basisFormat, width, height, hasAlpha ) { + const options = OPTIONS[ basisFormat ]; + for ( let i = 0; i < options.length; i++ ) { + const opt = options[ i ]; + if ( opt.if && ! config[ opt.if ] ) continue; + if ( ! opt.basisFormat.includes( basisFormat ) ) continue; + if ( hasAlpha && opt.transcoderFormat.length < 2 ) continue; + if ( opt.needsPowerOfTwo && ! ( isPowerOfTwo( width ) && isPowerOfTwo( height ) ) ) continue; + const transcoderFormat = opt.transcoderFormat[ hasAlpha ? 1 : 0 ]; + const engineFormat = opt.engineFormat[ hasAlpha ? 1 : 0 ]; + const engineType = opt.engineType[ 0 ]; + console.log(`opt: ${JSON.stringify(opt)}`); + + return { transcoderFormat, engineFormat, engineType }; + } + throw new Error( 'KTX2Loader: Failed to identify transcoding target.' ); + } + + function isPowerOfTwo( value ) { + if ( value <= 2 ) return true; + return ( value & ( value - 1 ) ) === 0 && value !== 0; + } + + /** Concatenates N byte arrays. */ + function concat( arrays ) { + if ( arrays.length === 1 ) return arrays[ 0 ]; + let totalByteLength = 0; + + for ( let i = 0; i < arrays.length; i ++ ) { + const array = arrays[ i ]; + totalByteLength += array.byteLength; + } + + const result = new Uint8Array( totalByteLength ); + let byteOffset = 0; + + for ( let i = 0; i < arrays.length; i ++ ) { + const array = arrays[ i ]; + result.set( array, byteOffset ); + byteOffset += array.byteLength; + } + + return result; + }"; +} + +@:structInit class BasisWorkerMessage { + public final id : String; + public final type = 'transcode'; + public final data : { + faces : Array<{ mipmaps : Array, width : Int, height : Int, format : Int, type : Int }>, + width : Int, + height : Int, + hasAlpha : Bool, + format : Int, + type : Int, + dfdFlags : Int, + }; + public final error : String = null; +} +#end \ No newline at end of file From 5419565801f5fc8611c397a6048b9595188b1571 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 15 Jan 2025 15:07:41 +0000 Subject: [PATCH 02/12] style: Formatting fix --- hxd/PixelFormat.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hxd/PixelFormat.hx b/hxd/PixelFormat.hx index ebbf9438c4..484b53c4d1 100644 --- a/hxd/PixelFormat.hx +++ b/hxd/PixelFormat.hx @@ -23,9 +23,9 @@ enum PixelFormat { RG16U; RGB16U; RGBA16U; - ASTC(v:Int); - ETC(v:Int); - S3TC(v:Int); + ASTC( v:Int ); + ETC( v:Int ); + S3TC( v:Int ); Depth16; Depth24; Depth24Stencil8; From 36b384bd77512fc5397d7a29bac5f8bbb3e307d2 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Wed, 15 Jan 2025 16:23:07 +0000 Subject: [PATCH 03/12] feat: Ktx2 support --- h3d/impl/GlDriver.hx | 64 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/h3d/impl/GlDriver.hx b/h3d/impl/GlDriver.hx index d771357b4d..e9fc4463dd 100644 --- a/h3d/impl/GlDriver.hx +++ b/h3d/impl/GlDriver.hx @@ -87,6 +87,15 @@ class GlDriver extends Driver { static var UID = 0; public var gl : GL; public static var ALLOW_WEBGL2 = true; + + public var textureSupport:{ + astc:Bool, + astcHDR:Bool, + etc1:Bool, + etc2:Bool, + dxt:Bool, + bptc:Bool, + }; #end #if (hlsdl||usegl) @@ -992,9 +1001,9 @@ class GlDriver extends Driver { case GL.RGB10_A2: GL.RGBA; case GL.RED, GL.R8, GL.R16F, GL.R32F, 0x822A: GL.RED; case GL.RG, GL.RG8, GL.RG16F, GL.RG32F, 0x822C: GL.RG; - case GL.RGB16F, GL.RGB32F, 0x8054, 0x8E8F: GL.RGB; - case 0x83F1, 0x83F2, 0x83F3, 0x805B, 0x8E8C: GL.RGBA; - default: throw "Invalid format " + t.internalFmt; + case GL.RGB16F, GL.RGB32F, 0x8054, hxd.CompressedTextureFormat.BPTC_FORMAT.RGB_BPTC_UNSIGNED, hxd.CompressedTextureFormat.ETC_FORMAT.RGB_ETC1: GL.RGB; + case 0x805B, hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT1,hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT3, + hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5,hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4, hxd.CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC : GL.RGBA; default: throw "Invalid format " + t.internalFmt; } } @@ -1083,8 +1092,7 @@ class GlDriver extends Driver { tt.internalFmt = GL.R11F_G11F_B10F; tt.pixelFmt = GL.UNSIGNED_INT_10F_11F_11F_REV; case S3TC(n) if( n <= maxCompressedTexturesSupport ): - if( t.width&3 != 0 || t.height&3 != 0 ) - throw "Compressed texture "+t+" has size "+t.width+"x"+t.height+" - must be a multiple of 4"; + checkMult4(t); switch( n ) { case 1: tt.internalFmt = 0x83F1; // COMPRESSED_RGBA_S3TC_DXT1_EXT case 2: tt.internalFmt = 0x83F2; // COMPRESSED_RGBA_S3TC_DXT3_EXT @@ -1093,6 +1101,18 @@ class GlDriver extends Driver { case 7: tt.internalFmt = 0x8E8C; // COMPRESSED_RGBA_BPTC_UNORM default: throw "Unsupported texture format "+t.format; } + case ASTC(n): + checkMult4(t); + switch (n) { + case 10: tt.internalFmt = hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4; + default: throw "Unsupported texture format " + t.format; + } + case ETC(n): + checkMult4(t); + switch (n) { + case 0: tt.internalFmt = hxd.CompressedTextureFormat.ETC_FORMAT.RGB_ETC1; + case 1: tt.internalFmt = hxd.CompressedTextureFormat.ETC_FORMAT.RGBA_ETC2; + } default: throw "Unsupported texture format "+t.format; } @@ -1169,6 +1189,11 @@ class GlDriver extends Driver { return tt; } + inline function checkMult4( t : h3d.mat.Texture ) { + if( t.width & 3 != 0 || t.height & 3 != 0 ) + throw "Compressed texture " + t + " has size " + t.width + "x" + t.height + " - must be a multiple of 4"; + } + function restoreBind() { var t = boundTextures[lastActiveIndex]; if( t == null ) @@ -1409,7 +1434,7 @@ class GlDriver extends Driver { case RGB10A2, RG11B10UF: new Uint32Array(@:privateAccess pixels.bytes.b.buffer, pixels.offset, dataLen>>2); default: new Uint8Array(@:privateAccess pixels.bytes.b.buffer, pixels.offset, dataLen); } - if( t.format.match(S3TC(_)) ) { + if( t.format.match(S3TC(_) | ASTC(_) | ETC(_)) ) { if( t.flags.has(IsArray) || t.flags.has(Is3D) ) gl.compressedTexSubImage3D(face, mipLevel, 0, 0, side, pixels.width, pixels.height, 1, t.t.internalFmt, buffer); else @@ -1893,20 +1918,37 @@ class GlDriver extends Driver { } #if js + public function checkTextureSupport() { + final checkExtension = ext -> { + gl.getExtension(ext) != null; + } + return { + astc: checkExtension('WEBGL_compressed_texture_astc'), + astcHDR: checkExtension('WEBGL_compressed_texture_astc') + && gl.getExtension('WEBGL_compressed_texture_astc').getSupportedProfiles().includes('hdr'), + etc1: false, // Not supported on WebGL2 (https://registry.khronos.org/OpenGL-Refpages/es3/html/glCompressedTexSubImage2D.xhtml); checkExtension('WEBGL_compressed_texture_etc1'), + etc2: checkExtension('WEBGL_compressed_texture_etc'), + dxt: checkExtension('WEBGL_compressed_texture_s3tc'), + bptc: checkExtension('EXT_texture_compression_bptc'), + } + } + var features : Map = new Map(); var has16Bits : Bool; function makeFeatures() { for( f in Type.allEnums(Feature) ) features.set(f,checkFeature(f)); - if( gl.getExtension("WEBGL_compressed_texture_s3tc") != null ) { - maxCompressedTexturesSupport = 3; - if( gl.getExtension("EXT_texture_compression_bptc") != null ) - maxCompressedTexturesSupport = 7; + textureSupport = checkTextureSupport(); + maxCompressedTexturesSupport = if( textureSupport.dxt || textureSupport.etc1 || textureSupport.etc2 || textureSupport.astc ) { + gl.getExtension("EXT_texture_compression_bptc") != null ? 7 : 3; + } else { + 3; } if( glES < 3 ) gl.getExtension("WEBGL_depth_texture"); has16Bits = gl.getExtension("EXT_texture_norm16") != null; // 16 bit textures } + function checkFeature( f : Feature ) { return switch( f ) { @@ -2126,4 +2168,4 @@ class GlDriver extends Driver { } -#end +#end \ No newline at end of file From 6340b23857d4c5e105669e5e142f49c9bdaa470c Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Mon, 27 Jan 2025 16:09:31 +0000 Subject: [PATCH 04/12] feat: Handle async loading of ktx2 textures --- hxd/PixelFormat.hx | 17 + hxd/Pixels.hx | 55 ++- hxd/impl/AsyncLoader.hx | 26 +- hxd/res/Image.hx | 125 ++++-- hxd/res/Ktx2.hx | 910 +++++++++++++++++++--------------------- 5 files changed, 596 insertions(+), 537 deletions(-) diff --git a/hxd/PixelFormat.hx b/hxd/PixelFormat.hx index 484b53c4d1..41f0fbe491 100644 --- a/hxd/PixelFormat.hx +++ b/hxd/PixelFormat.hx @@ -23,8 +23,25 @@ enum PixelFormat { RG16U; RGB16U; RGBA16U; + /** + Adaptive Scalable Texture Compression + - `10` 4x4 block size + */ ASTC( v:Int ); + /** + Ericsson Texture Compression + - `0` ETC1 (opaque RGB) + - `1` ETC2 (with alpha) + */ ETC( v:Int ); + /** + S3 Texture Compression (DXT/BC) + - `1`: BC1/DXT1 (opaque or 1-bit alpha) + - `2`: BC2/DXT3 (explicit alpha) + - `3`: BC3/DXT5 (interpolated alpha) + - `6`: BC6H (HDR, unsigned float) + - `7`: BC7 (high quality, alpha) + */ S3TC( v:Int ); Depth16; Depth24; diff --git a/hxd/Pixels.hx b/hxd/Pixels.hx index dc61fe4cd4..4dc8799805 100644 --- a/hxd/Pixels.hx +++ b/hxd/Pixels.hx @@ -390,10 +390,10 @@ class Pixels { nbytes.setFloat(i << 2, this.bytes.getFloat(i << 4)); this.bytes = nbytes; - case [S3TC(a),S3TC(b)] if( a == b ): - case [ASTC(a),ASTC(b)] if( a == b ): - case [ETC(a),ETC(b)] if( a == b ): - // nothing + case [S3TC(a),S3TC(b)] if( a == b ): // nothing + case [ASTC(a),ASTC(b)] if( a == b ): // Ktx2 will handle conversion + case [ETC(a),ETC(b)] if( a == b ): // Ktx2 will handle conversion + case [RGBA,_]: // With ktx2/basis textures, temp RGBA texture will be assigned before transcoding #if (hl && hl_ver >= "1.10") case [S3TC(ver),_]: @@ -407,7 +407,6 @@ class Pixels { convert(target); return; #end - default: throw "Cannot convert from " + format + " to " + target; } @@ -538,33 +537,33 @@ class Pixels { public static function calcDataSize( width : Int, height : Int, format : PixelFormat ) { return switch( format ) { - case S3TC(n): - var w = (width + 3) >> 2; + case S3TC(n): + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + var blocks = w * h; // Total number of blocks + if( n == 3 ) { // DXT5 + blocks * 16; // 16 bytes per block + } else if( n == 1 || n == 4 ) { + blocks * 8; // DXT1 or BC4, 8 bytes per block + } else { + blocks * 16; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity + } + case ASTC(n): + var w = (width + 3) >> 2; + var h = (height + 3) >> 2; + w * h * 16; + case ETC(n): + if( n == 0 ) { // RGB_ETC1_Format or RGB_ETC2_Format + var w = (width + 3) >> 2; var h = (height + 3) >> 2; - var blocks = w * h; // Total number of blocks - if( n == 3 ) { // DXT5 - blocks * 16; // 16 bytes per block - } else if( n == 1 || n == 4 ) { - blocks * 8; // DXT1 or BC4, 8 bytes per block - } else { - blocks * 16; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity - } - case ASTC(n): + w * h * 8; + } else if( n == 1 || n == 2 ) { // RGBA_ETC2_EAC_Format var w = (width + 3) >> 2; var h = (height + 3) >> 2; w * h * 16; - case ETC(n): - if( n == 0 ) { // RGB_ETC1_Format or RGB_ETC2_Format - var w = (width + 3) >> 2; - var h = (height + 3) >> 2; - w * h * 8; - } else if( n == 1 || n == 2 ) { // RGBA_ETC2_EAC_Format - var w = (width + 3) >> 2; - var h = (height + 3) >> 2; - w * h * 16; - } else { - throw "Unsupported ETC format"; - } + } else { + throw "Unsupported ETC format"; + } default: height * calcStride(width, format); } diff --git a/hxd/impl/AsyncLoader.hx b/hxd/impl/AsyncLoader.hx index 4e560ee7b6..c0dd52ea6c 100644 --- a/hxd/impl/AsyncLoader.hx +++ b/hxd/impl/AsyncLoader.hx @@ -1,6 +1,5 @@ package hxd.impl; - interface AsyncLoader { public function load( img : hxd.res.Image ) : Void; public function isSupported( t : hxd.res.Image ) : Bool; @@ -111,3 +110,28 @@ class NodeLoader implements AsyncLoader { } #end + +#if (js && !macro) +class PakLoader implements AsyncLoader { + + var fs : hxd.fmt.pak.FileSystem; + + public function new() { + fs = Std.downcast(hxd.res.Loader.currentInstance.fs, hxd.fmt.pak.FileSystem); + if( fs == null ) throw "Loader should be pak filesystem"; + } + + public function isSupported( img : hxd.res.Image ) { + return switch( img.getFormat() ) { + case Ktx2ETC1S, Ktx2UASTC, Png, Jpg: true; + default: false; + } + } + + public function load( img : hxd.res.Image ) { + final data = fs.get(img.entry.path).getBytes(); + @:privateAccess img.asyncLoad(data); + } + +} +#end diff --git a/hxd/res/Image.hx b/hxd/res/Image.hx index 75c04190eb..c183811592 100644 --- a/hxd/res/Image.hx +++ b/hxd/res/Image.hx @@ -1,5 +1,9 @@ package hxd.res; +import hxd.res.Ktx2.BasisFormat; +import hxd.res.Ktx2.BasisWorkerMessageData; +import hxd.res.Ktx2.TranscoderFormat; + enum abstract ImageFormat(Int) { var Jpg = 0; var Png = 1; @@ -8,7 +12,8 @@ enum abstract ImageFormat(Int) { var Dds = 4; var Raw = 5; var Hdr = 6; - var Ktx2 = 7; + var Ktx2ETC1S = 7; + var Ktx2UASTC = 8; /* Tells if we might not be able to directly decode the image without going through a loadBitmap async call. @@ -36,7 +41,8 @@ enum abstract ImageFormat(Int) { case Dds: "DDS"; case Raw: "RAW"; case Hdr: "HDR"; - case Ktx2: "KTX2"; + case Ktx2ETC1S: "KTX2ETC1S"; + case Ktx2UASTC: "KTX2UASTC"; }; } } @@ -249,15 +255,29 @@ class Image extends Resource { throw 'Use .ktx2 files for GPU compressed textures instead of .basis'; case 0x4BAB: final ktx2 = hxd.res.Ktx2.readFile(new haxe.io.BytesInput(@:privateAccess f.cache)); - inf.pixelFormat = switch ktx2.dfd.colorModel { - case hxd.res.Ktx2.DFDModel.ETC1S: ETC(hxd.res.Ktx2.TranscoderFormat.ETC1); - case hxd.res.Ktx2.DFDModel.UASTC: ASTC(hxd.res.Ktx2.TranscoderFormat.ASTC_4x4); - default: throw 'Unsupported colorModel in ktx2 file ${ktx2.dfd.colorModel}'; + final basisFormat = switch ktx2.dfd.colorModel { + case hxd.res.Ktx2.DFDModel.ETC1S: BasisFormat.ETC1S; + case hxd.res.Ktx2.DFDModel.UASTC: BasisFormat.UASTC; + default: throw 'Unsupported colorModel in ktx2 file: ${ktx2.dfd.colorModel}'; + } + final formatInfo = hxd.res.Ktx2.Ktx2Decoder.getTranscoderFormat(basisFormat, ktx2.header.pixelWidth, ktx2.header.pixelHeight, ktx2.dfd.hasAlpha()); + inf.pixelFormat = switch formatInfo.transcoderFormat { + case TranscoderFormat.ASTC_4x4: hxd.PixelFormat.ASTC(10); + case TranscoderFormat.BC7_M5: hxd.PixelFormat.S3TC(7); + case TranscoderFormat.BC3: hxd.PixelFormat.S3TC(3); + case TranscoderFormat.ETC1: hxd.PixelFormat.ETC(0); + case TranscoderFormat.ETC2: hxd.PixelFormat.ETC(1); + default: + throw 'Unsupported transcoder format: ${formatInfo.transcoderFormat}'; } inf.mipLevels = ktx2.header.levelCount; inf.width = ktx2.header.pixelWidth; inf.height = ktx2.header.pixelHeight; - inf.dataFormat = Ktx2; + inf.dataFormat = switch ktx2.dfd.colorModel { + case hxd.res.Ktx2.DFDModel.ETC1S: Ktx2ETC1S; + case hxd.res.Ktx2.DFDModel.UASTC: Ktx2UASTC; + default: throw 'Unsupported colorModel in ktx2 file ${ktx2.dfd.colorModel}'; + } #end case 0x3F23: // HDR RADIANCE @@ -453,7 +473,7 @@ class Image extends Resource { case Hdr: var data = hxd.fmt.hdr.Reader.decode(entry.getBytes(), false); pixels = new hxd.Pixels(data.width, data.height, data.bytes, inf.pixelFormat); - case Ktx2: + case Ktx2ETC1S, Ktx2UASTC: var bytes = entry.getBytes(); pixels = new hxd.Pixels(inf.width, inf.height, bytes, inf.pixelFormat); } @@ -551,17 +571,33 @@ class Image extends Resource { function asyncLoad(data:haxe.io.Bytes) { if (tex == null || tex.isDisposed()) return; - tex.dispose(); - tex.flags.unset(Loading); - @:privateAccess { - tex.format = inf.pixelFormat; - tex.width = inf.width; - tex.height = inf.height; + switch (inf.dataFormat) { + case Ktx2ETC1S, Ktx2UASTC: + final reader = new haxe.io.BytesInput(data); + hxd.res.Ktx2.Ktx2Decoder.getTranscodedData(reader, (data, header) -> { + tex.dispose(); + tex.flags.unset(Loading); + @:privateAccess { + tex.format = inf.pixelFormat; + tex.width = inf.width; + tex.height = inf.height; + } + loadTexture(null, data); + }); + default: + tex.dispose(); + tex.flags.unset(Loading); + @:privateAccess { + tex.format = inf.pixelFormat; + tex.width = inf.width; + tex.height = inf.height; + } + loadTexture(data); } - loadTexture(data); + } - function loadTexture(?asyncData:haxe.io.Bytes) { + function loadTexture(?asyncData:haxe.io.Bytes, ?asyncMessage:BasisWorkerMessageData) { if (getFormat().useLoadBitmap) { // use native decoding tex.flags.set(Loading); @@ -586,18 +622,23 @@ class Image extends Resource { } function load() { - if ((enableAsyncLoading || tex.flags.has(AsyncLoading)) && asyncData == null && ASYNC_LOADER != null && ASYNC_LOADER.isSupported(this)) - @:privateAccess { + tex.flags.set(AsyncLoading); // TODO: Set when loading ktx2 + if ((enableAsyncLoading || tex.flags.has(AsyncLoading)) && asyncData == null && asyncMessage == null && ASYNC_LOADER != null && ASYNC_LOADER.isSupported(this)) { + tex.dispose(); - tex.format = RGBA; - tex.width = 1; - tex.height = 1; - tex.customMipLevels = 1; + @:privateAccess tex.customMipLevels = 1; tex.flags.set(Loading); - tex.alloc(); - tex.uploadPixels(BLACK_1x1); - tex.width = inf.width; - tex.height = inf.height; + + if(tex.format.match(S3TC(_) | ASTC(_) | ETC(_))){ + tex.uploadPixels(Pixels.alloc(inf.width, inf.height, RGBA)); + } else { + @:privateAccess tex.format = RGBA; + @:privateAccess tex.width = 1; + @:privateAccess tex.height = 1; + tex.uploadPixels(BLACK_1x1); + @:privateAccess tex.width = inf.width; + @:privateAccess tex.height = inf.height; + } ASYNC_LOADER.load(this); tex.realloc = () -> loadTexture(); return; @@ -607,7 +648,7 @@ class Image extends Resource { @:privateAccess tex.customMipLevels = inf.mipLevels; tex.alloc(); switch (inf.dataFormat) { - case Dds: + case Dds: var pos = 128; if (inf.flags.has(Dxt10Header)) pos += 20; @@ -631,16 +672,30 @@ class Image extends Resource { pos += size; } } - case Ktx2: - throw 'Ktx2 loading using heaps resource system not implemented'; - default: - for (layer in 0...tex.layerCount) { - for (mip in 0...inf.mipLevels) { - var pixels = getPixels(tex.format, layer * inf.mipLevels + mip); - tex.uploadPixels(pixels, mip, layer); - pixels.dispose(); + case Ktx2ETC1S, Ktx2UASTC: + for (layer in 0...asyncMessage.faces.length) { + final face = asyncMessage.faces[layer]; + for (mip in 0...face.mipmaps.length) { + final w = inf.width >> mip; + final h = inf.height >> mip; + inf.pixelFormat = switch face.format { + case hxd.CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC: hxd.PixelFormat.S3TC(7); + case hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4: hxd.PixelFormat.ASTC(10); + default: throw 'No compressed texture format found for ${StringTools.hex(face.format)}'; } + final pixels = new hxd.Pixels(w, h, haxe.io.Bytes.ofData(face.mipmaps[mip].data.buffer), inf.pixelFormat, 0); + tex.uploadPixels(pixels, mip, layer); + pixels.dispose(); } + } + default: + for (layer in 0...tex.layerCount) { + for (mip in 0...inf.mipLevels) { + var pixels = getPixels(tex.format, layer * inf.mipLevels + mip); + tex.uploadPixels(pixels, mip, layer); + pixels.dispose(); + } + } } if (LOG_TEXTURE_LOAD && asyncData == null) { var time = (haxe.Timer.stamp() - t0) * 1000.0; diff --git a/hxd/res/Ktx2.hx b/hxd/res/Ktx2.hx index d67fb9fb56..704789a14a 100644 --- a/hxd/res/Ktx2.hx +++ b/hxd/res/Ktx2.hx @@ -1,7 +1,39 @@ package hxd.res; -#if js + import haxe.io.UInt8Array; using Lambda; + +#if js +typedef Promise = js.lib.Promise; +typedef ImageData = js.html.ImageData; +typedef Uint8Array = js.lib.Uint8Array; +typedef Worker = js.html.Worker; +#else +// TODO: Add support for native targets, just dummy typing for now... +class Promise { + public function new(cb:(resolve:T, reject:T) -> Void) {} + public function then(cb:(message:T) -> Void) {}; + static function resolve(thenable:Dynamic):Promise { + return null; + }; + static function reject(?reason:Dynamic):Promise { + return null; + + }; +} +typedef ImageData = Dynamic; +typedef Uint8Array = UInt8Array; +class Worker { + public var index:Int; + public function new(url:String) { + + } + + public function postMessage(message:Dynamic, ?transfer:Array) {} + dynamic public function onmessage(e : { data : { type : String, id : Int } }) {}; +} +#end + /** Ktx2 file parser. **/ @@ -23,7 +55,7 @@ class Ktx2 { header: header, levels: levels, dfd: dfd, - data: new js.lib.Uint8Array(@:privateAccess bytes.b), + data: new Uint8Array(cast @:privateAccess bytes.b), supercompressionGlobalData: null, } return file; @@ -181,6 +213,10 @@ class Ktx2 { } } + +/** + Handles transcoding of Ktx2 textures +**/ class Ktx2Decoder { public static var mscTranscoder : Dynamic; public static var workerLimit = 4; @@ -189,16 +225,25 @@ class Ktx2Decoder { static var _workerSourceURL : String; static var _workerConfig : BasisWorkerConfig; static var _workerPool : Array = []; - static var _transcoderPending : js.lib.Promise; + static var _transcoderPending : Promise; static var _transcoderBinary : haxe.io.Bytes; static var _transcoderScript : String; - static var _transcoderLoading : js.lib.Promise<{ script : String, wasm : haxe.io.Bytes }>; + static var _transcoderLoading : Promise<{ script : String, wasm : haxe.io.Bytes }>; - public static function getTexture(bytes : haxe.io.BytesInput, cb : (texture : h3d.mat.Texture) -> Void) { - createTexture(bytes, cb); + /** + Get transcoded texture + + @param bytes Texture data as BytesInput + @param cb Callback invoked when transcoding is done passing the transcoded texture. + **/ + public static function getTexture(bytes : haxe.io.BytesInput, cb : (texture : h3d.mat.Texture, header:KTX2Header) -> Void) { + getTranscodedData(bytes, (data, header) -> { + cb(createTexture(data, header), header); + }); } - static function detectSupport(fmt : KtxTranscodeTarget) { + static function detectSupport() { + #if js final driver : h3d.impl.GlDriver = cast h3d.Engine.getCurrent().driver; return { astcSupported: driver.textureSupport.astc, @@ -207,12 +252,15 @@ class Ktx2Decoder { dxtSupported: driver.textureSupport.dxt, bptcSupported: driver.textureSupport.bptc, } + #else + return null; + #end } - static function getWorker() { + static function getWorker():Promise { return initTranscoder().then(val -> { if( _workerPool.length < workerLimit ) { - final worker = new js.html.Worker(_workerSourceURL); + final worker = new Worker(_workerSourceURL); final workerTask : WorkerTask = { worker: worker, callbacks: new haxe.ds.IntMap(), @@ -245,33 +293,31 @@ class Ktx2Decoder { }); } - static function createTexture( buffer : haxe.io.BytesInput, cb : (texture:h3d.mat.Texture) -> Void) { - final ktx = Ktx2.readFile(buffer); - - final w = ktx.header.pixelWidth; - final h = ktx.header.pixelHeight; - - final transcodeTarget = switch ktx.dfd.colorModel { - case hxd.res.Ktx2.DFDModel.ETC1S: - KtxTranscodeTarget.ETC1S({}, { - fmt: CompressedFormat.ETC1, - alpha: ktx.dfd.hasAlpha(), - needsPowerOfTwo: true, - }); - case hxd.res.Ktx2.DFDModel.UASTC: - KtxTranscodeTarget.UASTC({}, { - fmt: CompressedFormat.ASTC, - alpha: ktx.dfd.hasAlpha(), - needsPowerOfTwo: true, - }); - default: throw 'Unsupported colorModel in ktx2 file ${ktx.dfd.colorModel}'; + public static function getTranscodedData(buffer:haxe.io.BytesInput, cb:(data:BasisWorkerMessageData, header:KTX2Header) -> Void) { + _workerConfig ??= detectSupport(); + if ( _workerConfig == null ) { + throw "Not implemented: Ktx2 only supported on js target"; } - _workerConfig = detectSupport(transcodeTarget); - getWorker().then(task -> { + + final ktx2File = Ktx2.readFile(buffer); + + // Determine basis format + final basisFormat = if ( ktx2File.header.vkFormat == 0 ) { + if ( ktx2File.dfd.colorModel == Ktx2.DFDModel.ETC1S ) ETC1S + else if ( ktx2File.dfd.colorModel == Ktx2.DFDModel.UASTC ) UASTC + else if ( ktx2File.dfd.transferFunction == Ktx2.DFDTransferFunction.LINEAR ) UASTC_HDR + else throw "KTX2Loader: Unknown Basis encoding"; + } else { + throw "KTX2Loader: Non-zero vkFormat not supported"; + }; + + // Get transcoder format + final formatInfo = getTranscoderFormat(basisFormat, ktx2File.header.pixelWidth, ktx2File.header.pixelHeight, ktx2File.dfd.hasAlpha()); + getWorker().then((task:WorkerTask) -> { final worker = task.worker; final taskID = _workerNextTaskID++; - final textureDone = new js.lib.Promise((resolve, reject) -> { + final textureDone = new Promise((resolve, reject) -> { task.callbacks.set(taskID, { resolve: resolve, reject: reject, @@ -280,82 +326,76 @@ class Ktx2Decoder { task.taskLoad += task.taskCosts.get(taskID); buffer.position = 0; final bytes = buffer.readAll().getData(); - worker.postMessage({type: 'transcode', id: taskID, buffer: bytes}, [bytes]); + worker.postMessage({ + type: 'transcode', + id: taskID, + buffer: bytes, + formatInfo: formatInfo, + }, [bytes]); }); - textureDone.then(( message : BasisWorkerMessage )-> { - if( message.type == 'error' ) { + textureDone.then((message:BasisWorkerMessage) -> { + if (message.type == 'error') { throw 'Unable to decode ktx2 file: ${message.error}'; } - - final w = message.data.width; - final h = message.data.height; - final create = (fmt:hxd.PixelFormat) -> { - if( ktx.header.faceCount > 1 || ktx.header.layerCount > 1 ) { - // TODO: Handle cube texture - throw 'Multi texture ktx2 files not supported'; - } - final face = message.data.faces[0]; - final mipmaps:Array = face.mipmaps; - final texture = new h3d.mat.Texture(w, h, null, fmt); - var level = 0; - for (mipmap in mipmaps) { - final bytes = haxe.io.Bytes.ofData(cast mipmap.data); - final pixels = new hxd.Pixels(mipmap.width, mipmap.height, bytes, fmt); - texture.uploadPixels(pixels, level); - level++; - } - if(mipmaps.length>1) { - texture.flags.set(MipMapped); - texture.mipMap = Linear; - } - texture; - } - final texture = switch message.data.format { - case EngineFormat.RGBA_ASTC_4x4_Format: - create(hxd.PixelFormat.ASTC(10)); - case EngineFormat.RGB_BPTC_UNSIGNED_Format: - create(hxd.PixelFormat.S3TC(6)); - case EngineFormat.RGBA_BPTC_Format: - create(hxd.PixelFormat.S3TC(7)); - case EngineFormat.RGBA_S3TC_DXT5_Format: - create(hxd.PixelFormat.S3TC(3)); - case EngineFormat.RGB_ETC1_Format: - create(hxd.PixelFormat.ETC(0)); - case EngineFormat.RGBA_ETC2_EAC_Format: - create(hxd.PixelFormat.ETC(1)); - default: - throw 'Ktx2Loader: No supported format available.'; - } - - if( task != null && taskID > 0 ) { - task.taskLoad -= task.taskCosts.get(taskID); - task.callbacks.remove(taskID); - task.taskCosts.remove(taskID); - } - cb(texture); + buffer.position = 0; + final header = Ktx2.readFile(buffer).header; + cb(message.data, header); }); }); - } + static function createTexture( data : BasisWorkerMessageData, header:KTX2Header ) { + final create = (fmt:hxd.PixelFormat) -> { + if( header.faceCount > 1 || header.layerCount > 1 ) { + // TODO: Handle cube texture + throw 'Multi texture ktx2 files not supported'; + } + final face = data.faces[0]; + final mipmaps:Array = face.mipmaps; + final texture = new h3d.mat.Texture(data.width, data.height, null, fmt); + var level = 0; + for ( mipmap in mipmaps ) { + final bytes = haxe.io.Bytes.ofData(cast mipmap.data); + final pixels = new hxd.Pixels(mipmap.width, mipmap.height, bytes, fmt); + texture.uploadPixels(pixels, level); + level++; + } + if( mipmaps.length>1 ) { + texture.flags.set(MipMapped); + texture.mipMap = Linear; + } + texture; + } + final texture = switch (data.format) { + case EngineFormat.RGBA_ASTC_4x4_Format: create(hxd.PixelFormat.ASTC(10)); + case EngineFormat.RGBA_BPTC_Format: create(hxd.PixelFormat.S3TC(7)); + case EngineFormat.RGBA_S3TC_DXT5_Format: create(hxd.PixelFormat.S3TC(3)); + case EngineFormat.RGB_ETC1_Format: create(hxd.PixelFormat.ETC(0)); + case EngineFormat.RGBA_ETC2_EAC_Format: create(hxd.PixelFormat.ETC(1)); + default: + throw 'Ktx2Loader: No supported format available.'; + } + return texture; + } + #if js static function initTranscoder() { _transcoderLoading = if( _transcoderLoading == null ) { // Load transcoder wrapper. final jsLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.js'); - final jsContent = new js.lib.Promise((resolve, reject) -> { + final jsContent = new Promise((resolve, reject) -> { jsLoader.onLoaded = resolve; jsLoader.onError = reject; jsLoader.load(); }); // Load transcoder WASM binary. final binaryLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.wasm'); - final binaryContent = new js.lib.Promise((resolve, reject) -> { + final binaryContent = new Promise((resolve, reject) -> { binaryLoader.onLoaded = resolve; binaryLoader.onError = reject; binaryLoader.load(true); }); - js.lib.Promise.all([jsContent, binaryContent]).then(arr -> {script:arr[0].toString(), wasm:arr[1]}); + Promise.all([jsContent, binaryContent]).then(arr -> {script:arr[0].toString(), wasm:arr[1]}); } else { _transcoderLoading; } @@ -363,33 +403,203 @@ class Ktx2Decoder { _transcoderPending = _transcoderLoading.then(o -> { _transcoderScript = o.script; _transcoderBinary = o.wasm; - final transcoderFormat = Type.getClassFields(TranscoderFormat).map(f -> '"$f": ${Reflect.field(TranscoderFormat, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; - final basisFormat = Type.allEnums(BasisFormat).fold((curr, acc) -> '$acc\t"${curr.getName()}": ${curr.getIndex()},\n', '{\n') + '}'; - final engineFormat = Type.getClassFields(EngineFormat).map(f -> '"$f": ${Reflect.field(EngineFormat, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; - final engineType = Type.getClassFields(EngineType).map(f -> '"$f": ${Reflect.field(EngineType, f)},\n').fold((curr, acc) -> '$acc\t$curr', '{\n') + '}'; - final body = [ - '/* constants */', - 'let _EngineFormat = $engineFormat', - 'let _EngineType = $engineType', - 'let _TranscoderFormat = $transcoderFormat', - 'let _BasisFormat = $basisFormat', - '/* basis_transcoder.js */', - _transcoderScript, - '/* worker */', - basisWorker() - ].join('\n'); - - _workerSourceURL = js.html.URL.createObjectURL(new js.html.Blob([body])); + final workerScript = ' + let config; + let transcoderPending; + let formatInfo; + let BasisModule; + // Inject the basis transcoder script into the workers context + ${_transcoderScript} + + self.onmessage = function(e) { + const message = e.data; + switch (message.type) { + case "init": + config = message.config; + init(message.transcoderBinary); + break; + case "transcode": + formatInfo = message.formatInfo; + transcoderPending.then(function() { + try { + const { faces, buffers, width, height, hasAlpha, format, type, dfdFlags } = transcode( message.buffer ); + self.postMessage( { type: "transcode", id: message.id, data: { faces, width, height, hasAlpha, format, type, dfdFlags } }, buffers ); + } catch (error) { + self.postMessage({ type: "error", id: message.id, error: error.message }); + } + }); + break; + } + }; + + function init(wasmBinary) { + transcoderPending = new Promise(function(resolve) { + BasisModule = { wasmBinary, onRuntimeInitialized: resolve }; + BASIS(BasisModule); + }).then(function() { + BasisModule.initializeBasis(); + }); + } + + /** Concatenates N byte arrays. */ + function concat( arrays ) { + if ( arrays.length === 1 ) return arrays[ 0 ]; + let totalByteLength = 0; + for ( let i = 0; i < arrays.length; i ++ ) { + const array = arrays[ i ]; + totalByteLength += array.byteLength; + } + const result = new Uint8Array( totalByteLength ); + let byteOffset = 0; + for ( let i = 0; i < arrays.length; i ++ ) { + const array = arrays[ i ]; + result.set( array, byteOffset ); + byteOffset += array.byteLength; + } + return result; + } + + function transcode(buffer) { + let ktx2File = new BasisModule.KTX2File(new Uint8Array(buffer)); + function cleanup() { + ktx2File.close(); + ktx2File.delete(); + } + if ( ! ktx2File.isValid() ) { + cleanup(); + throw new Error( "KTX2Loader: Invalid or unsupported .ktx2 file" ); + } + let basisFormat; + if ( ktx2File.isUASTC() ) { + basisFormat = ${BasisFormat.UASTC.getIndex()}; + } else if ( ktx2File.isETC1S() ) { + basisFormat = ${BasisFormat.ETC1S.getIndex()}; + } else if ( ktx2File.isHDR() ) { + basisFormat = ${BasisFormat.UASTC_HDR.getIndex()}; + } else { + throw new Error( "KTX2Loader: Unknown Basis encoding" ); + } + const width = ktx2File.getWidth(); + const height = ktx2File.getHeight(); + const layerCount = ktx2File.getLayers() || 1; + const levelCount = ktx2File.getLevels(); + const faceCount = ktx2File.getFaces(); + const hasAlpha = ktx2File.getHasAlpha(); + const dfdFlags = ktx2File.getDFDFlags(); + const { transcoderFormat, engineFormat, engineType } = formatInfo; + if ( ! width || ! height || ! levelCount ) { + cleanup(); + throw new Error("KTX2Loader: Invalid texture ktx2File:" + JSON.stringify(ktx2File)); + } + if ( ! ktx2File.startTranscoding() ) { + cleanup(); + throw new Error( "KTX2Loader: .startTranscoding failed" ); + } + const faces = []; + const buffers = []; + for ( let face = 0; face < faceCount; face ++ ) { + const mipmaps = []; + for ( let mip = 0; mip < levelCount; mip ++ ) { + const layerMips = []; + let mipWidth, mipHeight; + for ( let layer = 0; layer < layerCount; layer ++ ) { + const levelInfo = ktx2File.getImageLevelInfo( mip, layer, face ); + if ( face === 0 && mip === 0 && layer === 0 && ( levelInfo.origWidth % 4 !== 0 || levelInfo.origHeight % 4 !== 0 ) ) { + console.warn( "KTX2Loader: ETC1S and UASTC textures should use multiple-of-four dimensions." ); + } + if ( levelCount > 1 ) { + mipWidth = levelInfo.origWidth; + mipHeight = levelInfo.origHeight; + } else { + // Handles non-multiple-of-four dimensions in textures without mipmaps. Textures with + // mipmaps must use multiple-of-four dimensions, for some texture formats and APIs. + // See mrdoob/three.js#25908. + mipWidth = levelInfo.width; + mipHeight = levelInfo.height; + } + let dst = new Uint8Array( ktx2File.getImageTranscodedSizeInBytes( mip, layer, 0, transcoderFormat ) ); + const status = ktx2File.transcodeImage( dst, mip, layer, face, transcoderFormat, 0, - 1, - 1 ); + if ( engineType === ${EngineType.HalfFloatType} ) { + dst = new Uint16Array( dst.buffer, dst.byteOffset, dst.byteLength / Uint16Array.BYTES_PER_ELEMENT ); + } + if ( ! status ) { + cleanup(); + throw new Error( "KTX2Loader: .transcodeImage failed." ); + } + layerMips.push( dst ); + } + const mipData = concat( layerMips ); + mipmaps.push( { data: mipData, width: mipWidth, height: mipHeight } ); + buffers.push( mipData.buffer ); + } + faces.push( { mipmaps, width, height, format: engineFormat, type: engineType } ); + } + cleanup(); + return { faces, buffers, width, height, hasAlpha, dfdFlags, format: engineFormat, type: engineType }; + } + '; + + final blob = new js.html.Blob([workerScript], {type: "text/javascript"}); + _workerSourceURL = js.Syntax.code("URL.createObjectURL({0})", blob); }); return _transcoderPending; } + #else + static function initTranscoder() { + return null; + } + #end + public static function getTranscoderFormat(basisFormat:BasisFormat, width:Int, height:Int, hasAlpha:Bool):{ transcoderFormat:Int, engineFormat:Int, engineType:Int } { + _workerConfig ??= detectSupport(); + final caps = _workerConfig; + return switch basisFormat { + case BasisFormat.ETC1S if(hasAlpha && caps.etc2Supported): + { transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.ETC1S if(!hasAlpha && (caps.etc1Supported || caps.etc2Supported)): + { transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.ETC1S if(caps.bptcSupported): + { transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.ETC1S if(hasAlpha && caps.dxtSupported): + { transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.ETC1S if(!hasAlpha && caps.dxtSupported): + { transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.ETC1S: + { transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType }; + + case BasisFormat.UASTC if(caps.astcSupported): + { transcoderFormat: TranscoderFormat.ASTC_4x4, engineFormat: EngineFormat.RGBA_ASTC_4x4_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC if(caps.bptcSupported): + { transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC if(hasAlpha && caps.etc2Supported): + { transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC if((!hasAlpha && caps.etc2Supported) || caps.etc1Supported): + { transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC if(hasAlpha && caps.dxtSupported): + { transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC if(!hasAlpha && caps.dxtSupported): + { transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType }; + case BasisFormat.UASTC: + { transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType }; + + case BasisFormat.UASTC_HDR: + { transcoderFormat: TranscoderFormat.RGBA_HALF, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.HalfFloatType }; + + case _: + throw 'KTX2Loader: Failed to identify transcoding target.'; + } + } + + static function isPowerOfTwo(value:Int):Bool { + return value > 0 && (value & (value - 1)) == 0; + } + } typedef Ktx2File = { header : KTX2Header, levels : Array, dfd : KTX2DFD, - data : js.lib.Uint8Array, + data : Uint8Array, supercompressionGlobalData : KTX2SupercompressionGlobalData, } @@ -431,22 +641,71 @@ enum abstract SupercompressionScheme(Int) from Int to Int { public final ZLib = 3; } -/** @internal */ +/** + Ktx2 file header + + See https://github.khronos.org/KTX-Specification/ktxspec.v2.html for detailed information. +**/ @:structInit class KTX2Header { + /** + Vulkan format. Will be 0 for universal texture formats. + **/ public final vkFormat : Int; + /** + Size of data type in bytes used to upload data to a graphics API. + **/ public final typeSize : Int; + /** + The width of texture image for level 0, in pixels. + **/ public final pixelWidth : Int; + /** + The height of texture image for level 0, in pixels. + **/ public final pixelHeight : Int; + /** + The depth of texture image for level 0, in pixels. + **/ public final pixelDepth : Int; + /** + Number of array elements. If texture is not an array texture, layerCount must equal 0. + **/ public final layerCount : Int; + /** + Number of cubemap faces. For cubemaps and cubemap arrays this must be 6. For non cubemaps this must be 1. + **/ public final faceCount : Int; + /** + Specifies number of mip levels. + **/ public final levelCount : Int; + /*** + Indicates if supercompression scheme has been applied. 0=None, 1=BasisLZ, 2=Zstandard, 3=ZLIB + **/ public final supercompressionScheme : Int; + /** + Offset from start of file for dfdTotalSize field in Data Format Descriptor + **/ public final dfdByteOffset : Int; + /** + Total number of bytes in the Data Format Descriptor, including dfdTotalSize field. + **/ public final dfdByteLength : Int; + /** + Offset of key/value pair data + **/ public final kvdByteOffset : Int; + /** + Total number of bytes of key/value data + **/ public final kvdByteLength : Int; + /** + The offset from the start of the file of supercompressionGlobalData. + **/ public final sgdByteOffset : Int; + /** + Number of bytes of supercompressionGlobalData. + **/ public final sgdByteLength : Int; public function needZSTDDecoder() { @@ -454,11 +713,10 @@ enum abstract SupercompressionScheme(Int) from Int to Int { } } -/** @internal */ typedef KTX2Level = { /** Byte offset. According to spec this should be 64 bit, but since a lot of byte code in haxe is using regular 32 bit Int for indexing, - supporting files to large to fit in 32bit space is complicated and should not be needed for individual game assets. + supporting files too large to fit in 32bit space is complicated and should not be needed for individual game assets. **/ final byteOffset : Int; final byteLength : Int; @@ -475,26 +733,67 @@ typedef KTX2Sample = { final sampleUpper : Int; } -/** @internal */ +/** Ktx2 Document Format Description */ @:structInit class KTX2DFD { + /** + Defined as 0 in spec + **/ public final vendorId : Int; + /** + Defined as 0 in spec + **/ public final descriptorType : Int; + /** + Defined as 2 in spec for ktx2 + **/ public final versionNumber : Int; + /** + Size in bytes of this Descriptor Block + **/ public final descriptorBlockSize : Int; + /** + Color model for encoded data (ETC1S=163, UASTC=166) + **/ public final colorModel : Int; + /** + Color primaries used when encoding. BT709/SRGB (1) recommended for standard dynamic range, standard gamut images. See KHR_DF_PRIMARIES in khr_df.h for other values. + **/ public final colorPrimaries : Int; + /** + Encoding curve used to map luminance, with values like KHR_DF_TRANSFER_LINEAR, KHR_DF_TRANSFER_SRGB, or KHR_DF_TRANSFER_ST2084 for HDR. See KHR_DF_TRANSFER in khr_df.h for other values. + **/ public final transferFunction : Int; + /** + Indicates if premultiplied aplha should be used. KHR_DF_FLAG_ALPHA_PREMULTIPLIED (1) for PMA, or KHR_DF_FLAG_ALPHA_STRAIGHT (0) for non-PMA. + **/ public final flags : Int; + /** + Integer bound on range of coordinates covered by repeating block described by samples. Four separate values, represented as unsigned 8-bit integers, are supported, corresponding to successive dimensions. + **/ public final texelBlockDimension : { x : Int, y : Int, z : Int, w : Int, }; + /** + Number of bytes which a plane contributes to the format. + **/ public final bytesPlane : Array; + /** + Number of samples present in the format. + **/ public final numSamples : Int; + /** + Samples data + **/ public final samples : Array; + /** + Check if texture data has alpha channel + + @return True if file has alpha channel + **/ public function hasAlpha() { return switch colorModel { case hxd.res.Ktx2.DFDModel.ETC1S: @@ -505,12 +804,16 @@ typedef KTX2Sample = { } } + /** + Check if texture data is in gamma space. + + @return True if texture is in gamma space + **/ public function isInGammaSpace() { return transferFunction == DFDTransferFunction.SRGB; } } -/** @internal */ typedef KTX2ImageDesc = { final imageFlags : Int; final rgbSliceByteOffset : Int; @@ -519,7 +822,6 @@ typedef KTX2ImageDesc = { final alphaSliceByteLength : Int; } -/** @internal */ typedef KTX2SupercompressionGlobalData = { final endpointCount : Int; final selectorCount : Int; @@ -559,22 +861,22 @@ class TranscoderFormat { enum TranscoderType { cTFETC1; - cTFETC2; // Not used + cTFETC2; cTFBC1; cTFBC3; - cTFBC4; // Not used - cTFBC5; // Not used - cTFBC7_M6_OPAQUE_ONLY; // Not used - cTFBC7_M5; // Not used - cTFPVRTC1_4_RGB; // Not used - cTFPVRTC1_4_RGBA; // Not used + cTFBC4; + cTFBC5; + cTFBC7_M6_OPAQUE_ONLY; + cTFBC7_M5; + cTFPVRTC1_4_RGB; + cTFPVRTC1_4_RGBA; cTFASTC_4x4; - cTFATC_RGB1; // Not used - cTFATC_RGBA_INTERPOLATED_ALPHA2; // Not used + cTFATC_RGB1; + cTFATC_RGBA_INTERPOLATED_ALPHA2; cTFRGBA321; - cTFRGB5654; // Not used - cTFBGR5655; // Not used - cTFRGBA44446; // Not used + cTFRGB5654; + cTFBGR5655; + cTFRGBA44446; } @:keep @@ -628,70 +930,8 @@ class EngineType { public final height : Int; } -enum KtxTranscodeTarget { - ETC1S(options : ETC1SDecoderOptions, caps : Ktx2Caps); - UASTC(options : UASTCDecoderOptions, caps : Ktx2Caps); -} - -@:structInit class KtxTranscodeConfig { - public final transcodeFormat :TranscodeTarget; - public final engineFormat :Int; - public final engineType = EngineType.UnsignedByteType; - public final roundToMultiple4 = true; -} - - -@:structInit class Ktx2Caps { - public final fmt : CompressedFormat; - - public final alpha : Null = null; - - public final needsPowerOfTwo = true; -} - -enum CompressedFormat { - ETC2; - ETC1; - S3TC; - ASTC; - BPTC; -} - -/** -* Options passed to the KTX2 decode function -*/ -@:structInit class UASTCDecoderOptions { - /** use RGBA format if ASTC and BC7 are not available as transcoded format */ - public final useRGBAIfASTCBC7NotAvailableWhenUASTC = false; - - /** force to always use (uncompressed) RGBA for transcoded format */ - public final forceRGBA = false; - - /** force to always use (uncompressed) R8 for transcoded format */ - public final forceR8 = false; - - /** force to always use (uncompressed) RG8 for transcoded format */ - public final forceRG8 = false; -} - -@:structInit class ETC1SDecoderOptions { - public final forceRGBA = false; -} - -enum TranscodeTarget { - ASTC_4X4_RGBA; - BC7_RGBA; - BC3_RGBA; - BC1_RGB; - ETC2_RGBA; - ETC1_RGB; - RGBA32; - R8; - RG8; -} - typedef WorkerTask = { - worker : js.html.Worker, + worker : Worker, callbacks : haxe.ds.IntMap<{ resolve : (value : Dynamic) -> Void, reject : (reason : Dynamic) -> Void }>, taskCosts : haxe.ds.IntMap, taskLoad : Int, @@ -702,298 +942,22 @@ typedef BasisWorkerConfig = { etc1Supported : Bool, etc2Supported : Bool, dxtSupported : Bool, -} - -function basisWorker() { - return " - let config; - let transcoderPending; - let BasisModule; - - const EngineFormat = _EngineFormat; - const EngineType = _EngineType; - const TranscoderFormat = _TranscoderFormat; - const BasisFormat = _BasisFormat; - - self.addEventListener( 'message', function ( e ) { - const message = e.data; - switch ( message.type ) { - case 'init': - config = message.config; - init( message.transcoderBinary ); - break; - case 'transcode': - transcoderPending.then( () => { - try { - const { faces, buffers, width, height, hasAlpha, format, type, dfdFlags } = transcode( message.buffer ); - self.postMessage( { type: 'transcode', id: message.id, data: { faces, width, height, hasAlpha, format, type, dfdFlags } }, buffers ); - } catch ( error ) { - console.error( error ); - self.postMessage( { type: 'error', id: message.id, error: error.message } ); - } - } ); - break; - } - } ); - - function init( wasmBinary ) { - transcoderPending = new Promise( ( resolve ) => { - BasisModule = { wasmBinary, onRuntimeInitialized: resolve }; - BASIS( BasisModule ); // eslint-disable-line no-undef - } ).then( () => { - BasisModule.initializeBasis(); - if ( BasisModule.KTX2File === undefined ) { - console.warn( 'KTX2Loader: Please update Basis Universal transcoder.' ); - } - } ); - } - - function transcode( buffer ) { - const ktx2File = new BasisModule.KTX2File( new Uint8Array( buffer ) ); - function cleanup() { - ktx2File.close(); - ktx2File.delete(); - } - - if ( ! ktx2File.isValid() ) { - cleanup(); - throw new Error( 'KTX2Loader: Invalid or unsupported .ktx2 file' ); - } - let basisFormat; - if ( ktx2File.isUASTC() ) { - basisFormat = BasisFormat.UASTC; - } else if ( ktx2File.isETC1S() ) { - basisFormat = BasisFormat.ETC1S; - } else if ( ktx2File.isHDR() ) { - basisFormat = BasisFormat.UASTC_HDR; - } else { - throw new Error( 'KTX2Loader: Unknown Basis encoding' ); - } - const width = ktx2File.getWidth(); - const height = ktx2File.getHeight(); - const layerCount = ktx2File.getLayers() || 1; - const levelCount = ktx2File.getLevels(); - const faceCount = ktx2File.getFaces(); - const hasAlpha = ktx2File.getHasAlpha(); - const dfdFlags = ktx2File.getDFDFlags(); - const { transcoderFormat, engineFormat, engineType } = getTranscoderFormat( basisFormat, width, height, hasAlpha ); - if ( ! width || ! height || ! levelCount ) { - cleanup(); - throw new Error( `KTX2Loader: Invalid texture ktx2File:${JSON.stringify(ktx2File)} w:${width} h: ${height} levelCount:${levelCount}` ); - } - - if ( ! ktx2File.startTranscoding() ) { - cleanup(); - throw new Error( 'KTX2Loader: .startTranscoding failed' ); - } - - const faces = []; - const buffers = []; - - for ( let face = 0; face < faceCount; face ++ ) { - const mipmaps = []; - for ( let mip = 0; mip < levelCount; mip ++ ) { - const layerMips = []; - let mipWidth, mipHeight; - for ( let layer = 0; layer < layerCount; layer ++ ) { - const levelInfo = ktx2File.getImageLevelInfo( mip, layer, face ); - if ( face === 0 && mip === 0 && layer === 0 && ( levelInfo.origWidth % 4 !== 0 || levelInfo.origHeight % 4 !== 0 ) ) { - console.warn( 'KTX2Loader: ETC1S and UASTC textures should use multiple-of-four dimensions.' ); - } - - if ( levelCount > 1 ) { - mipWidth = levelInfo.origWidth; - mipHeight = levelInfo.origHeight; - } else { - // Handles non-multiple-of-four dimensions in textures without mipmaps. Textures with - // mipmaps must use multiple-of-four dimensions, for some texture formats and APIs. - // See mrdoob/three.js#25908. - mipWidth = levelInfo.width; - mipHeight = levelInfo.height; - } - - let dst = new Uint8Array( ktx2File.getImageTranscodedSizeInBytes( mip, layer, 0, transcoderFormat ) ); - const status = ktx2File.transcodeImage( dst, mip, layer, face, transcoderFormat, 0, - 1, - 1 ); - - if ( engineType === EngineType.HalfFloatType ) { - dst = new Uint16Array( dst.buffer, dst.byteOffset, dst.byteLength / Uint16Array.BYTES_PER_ELEMENT ); - } - - if ( ! status ) { - cleanup(); - throw new Error( 'KTX2Loader: .transcodeImage failed.' ); - } - layerMips.push( dst ); - } - const mipData = concat( layerMips ); - mipmaps.push( { data: mipData, width: mipWidth, height: mipHeight } ); - buffers.push( mipData.buffer ); - } - faces.push( { mipmaps, width, height, format: engineFormat, type: engineType } ); - } - cleanup(); - return { faces, buffers, width, height, hasAlpha, dfdFlags, format: engineFormat, type: engineType }; - } - // - - // Optimal choice of a transcoder target format depends on the Basis format (ETC1S, UASTC, or - // UASTC HDR), device capabilities, and texture dimensions. The list below ranks the formats - // separately for each format. Currently, priority is assigned based on: - // - // high quality > low quality > uncompressed - // - // Prioritization may be revisited, or exposed for configuration, in the future. - // - // Reference: https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXDeveloperGuide.md - const FORMAT_OPTIONS = [ - { - if: 'astcSupported', - basisFormat: [ BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4 ], - engineFormat: [ EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format ], - engineType: [ EngineType.UnsignedByteType ], - priorityETC1S: Infinity, - priorityUASTC: 1, - needsPowerOfTwo: false, - }, - { - if: 'bptcSupported', - basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5 ], - engineFormat: [ EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format ], - engineType: [ EngineType.UnsignedByteType ], - priorityETC1S: 3, - priorityUASTC: 2, - needsPowerOfTwo: false, - }, - { - if: 'dxtSupported', - basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.BC1, TranscoderFormat.BC3 ], - engineFormat: [ EngineFormat.RGBA_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format ], - engineType: [ EngineType.UnsignedByteType ], - priorityETC1S: 4, - priorityUASTC: 5, - needsPowerOfTwo: false, - }, - { - if: 'etc2Supported', - basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.ETC1, TranscoderFormat.ETC2 ], - engineFormat: [ EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format ], - engineType: [ EngineType.UnsignedByteType ], - priorityETC1S: 1, - priorityUASTC: 3, - needsPowerOfTwo: false, - }, - { - if: 'etc1Supported', - basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.ETC1 ], - engineFormat: [ EngineFormat.RGB_ETC1_Format ], - engineType: [ EngineType.UnsignedByteType ], - priorityETC1S: 2, - priorityUASTC: 4, - needsPowerOfTwo: false, - }, - - // Uncompressed fallbacks. - - { - basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC ], - transcoderFormat: [ TranscoderFormat.RGBA32, TranscoderFormat.RGBA32 ], - engineFormat: [ EngineFormat.RGBAFormat, EngineFormat.RGBAFormat ], - engineType: [ EngineType.UnsignedByteType, EngineType.UnsignedByteType ], - priorityETC1S: 100, - priorityUASTC: 100, - needsPowerOfTwo: false, - }, - { - basisFormat: [ BasisFormat.UASTC_HDR ], - transcoderFormat: [ TranscoderFormat.RGBA_HALF ], - engineFormat: [ EngineFormat.RGBAFormat ], - engineType: [ EngineType.HalfFloatType ], - priorityHDR: 100, - needsPowerOfTwo: false, - } - ]; - - const OPTIONS = { - // TODO: For ETC1S we intentionally sort by _UASTC_ priority, preserving - // a historical accident shown to avoid performance pitfalls for Linux with - // Firefox & AMD GPU (RadeonSI). Further work needed. - // See https://github.com/mrdoob/three.js/pull/29730. - [ BasisFormat.ETC1S ]: FORMAT_OPTIONS - .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.ETC1S ) ) - .sort( ( a, b ) => a.priorityUASTC - b.priorityUASTC ), - - [ BasisFormat.UASTC ]: FORMAT_OPTIONS - .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.UASTC ) ) - .sort( ( a, b ) => a.priorityUASTC - b.priorityUASTC ), - - [ BasisFormat.UASTC_HDR ]: FORMAT_OPTIONS - .filter( ( opt ) => opt.basisFormat.includes( BasisFormat.UASTC_HDR ) ) - .sort( ( a, b ) => a.priorityHDR - b.priorityHDR ), - }; - - function getTranscoderFormat( basisFormat, width, height, hasAlpha ) { - const options = OPTIONS[ basisFormat ]; - for ( let i = 0; i < options.length; i++ ) { - const opt = options[ i ]; - if ( opt.if && ! config[ opt.if ] ) continue; - if ( ! opt.basisFormat.includes( basisFormat ) ) continue; - if ( hasAlpha && opt.transcoderFormat.length < 2 ) continue; - if ( opt.needsPowerOfTwo && ! ( isPowerOfTwo( width ) && isPowerOfTwo( height ) ) ) continue; - const transcoderFormat = opt.transcoderFormat[ hasAlpha ? 1 : 0 ]; - const engineFormat = opt.engineFormat[ hasAlpha ? 1 : 0 ]; - const engineType = opt.engineType[ 0 ]; - console.log(`opt: ${JSON.stringify(opt)}`); - - return { transcoderFormat, engineFormat, engineType }; - } - throw new Error( 'KTX2Loader: Failed to identify transcoding target.' ); - } - - function isPowerOfTwo( value ) { - if ( value <= 2 ) return true; - return ( value & ( value - 1 ) ) === 0 && value !== 0; - } - - /** Concatenates N byte arrays. */ - function concat( arrays ) { - if ( arrays.length === 1 ) return arrays[ 0 ]; - let totalByteLength = 0; - - for ( let i = 0; i < arrays.length; i ++ ) { - const array = arrays[ i ]; - totalByteLength += array.byteLength; - } - - const result = new Uint8Array( totalByteLength ); - let byteOffset = 0; - - for ( let i = 0; i < arrays.length; i ++ ) { - const array = arrays[ i ]; - result.set( array, byteOffset ); - byteOffset += array.byteLength; - } - - return result; - }"; + bptcSupported : Bool, } @:structInit class BasisWorkerMessage { public final id : String; public final type = 'transcode'; - public final data : { - faces : Array<{ mipmaps : Array, width : Int, height : Int, format : Int, type : Int }>, - width : Int, - height : Int, - hasAlpha : Bool, - format : Int, - type : Int, - dfdFlags : Int, - }; + public final data : BasisWorkerMessageData; public final error : String = null; } -#end \ No newline at end of file + +typedef BasisWorkerMessageData = { + faces : Array<{ mipmaps : Array, width : Int, height : Int, format : Int, type : Int }>, + width : Int, + height : Int, + hasAlpha : Bool, + format : Int, + type : Int, + dfdFlags : Int, +} From 059f542ce60be6cec81ac0c187215b9fcfe7f3f9 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Tue, 28 Jan 2025 13:18:06 +0000 Subject: [PATCH 05/12] feat: Add ktx2 convert --- hxd/fmt/pak/Build.hx | 5 +++-- hxd/fs/Convert.hx | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/hxd/fmt/pak/Build.hx b/hxd/fmt/pak/Build.hx index 1722745b9e..4400a6a1bb 100644 --- a/hxd/fmt/pak/Build.hx +++ b/hxd/fmt/pak/Build.hx @@ -188,11 +188,12 @@ class Build { f.close(); } - public static function make( dir = "res", out = "res", ?pakDiff ) { + public static function make( dir = "res", out = "res", ?pakDiff, ?config ) { var b = new Build(); b.resPath = dir; - b.outPrefix = out; + b.outPrefix = config != null ? '$out.$config' : out; b.pakDiff = pakDiff; + b.configuration = config; b.makePak(); } diff --git a/hxd/fs/Convert.hx b/hxd/fs/Convert.hx index ceb2becf07..a63ae3d37d 100644 --- a/hxd/fs/Convert.hx +++ b/hxd/fs/Convert.hx @@ -425,4 +425,46 @@ class ConvertSVGToMSDF extends Convert { static var _ = Convert.register(new ConvertSVGToMSDF("svg", "png")); } + +class ConvertPngToKtx2 extends hxd.fs.Convert { + override function convert() { + final format = params.dataFormat ?? 'R8G8B8A8_SRGB'; + final oetf = params.assignOetf ?? 'srgb'; + final primaries = params.assignPrimaries ?? 'bt709'; + final cmd = if ( params.format == 'etc1s' ) { + final clevel = params.clevel ?? 3; + final qlevel = params.qlevel ?? 128; + 'ktx create --encode basis-lz --format ${format} --assign-oetf ${oetf} --clevel ${clevel} --qlevel ${qlevel} --warn-on-color-conversions --compare-psnr ${srcPath} ${dstPath}'; + + } else { + final quality = params.quality ?? 2; + 'ktx create --encode uastc --zstd 18 --format ${format} --assign-oetf ${oetf} --uastc-quality ${quality} --warn-on-color-conversions --compare-psnr ${srcPath} ${dstPath}'; + } + final scProcess = new sys.io.Process(cmd); + final errorMsg = 'Error compressing $srcPath to ktx2: ${scProcess.stderr.readAll().toString()}'; + if ( scProcess.exitCode() != 0 ) { + throw errorMsg; + } + final regexp = ~/PSNR Max: (.+)/; + final result = scProcess.stdout.readAll().toString(); + final snr = regexp.match(result) ? regexp.matched(1) : null; + #if log_ktx2_snr Sys.println(' Converted ${haxe.io.Path.withoutDirectory(srcPath)} with PSNR Max: ${snr}'); #end + if ( params.snrThreshold != null && params.format == 'etc1s' ) { + if ( Std.parseFloat(snr) < params.snrThreshold ) { + params.format = 'uastc'; + // If signal to noise ratio is too low, discard ETC1S and use UASTC encoding instead + sys.FileSystem.deleteFile(dstPath); + trace('⚠️⚠️⚠️ Low signal to noise ratio when encoding $srcPath as ETC1S. Will fall back to using UASTC with default settings. Update props.json to use "uastc" as format, or ajust or remove "snrThreshold" to make it pass verification. ⚠️⚠️⚠️'); + final process = new sys.io.Process('ktx create --encode uastc --zstd 18 --format ${format} --assign-oetf ${oetf} --uastc-quality 2 --warn-on-color-conversions ${srcPath} ${dstPath}'); + if ( process.exitCode() != 0 ) { + throw errorMsg; + } + } + } + + } + + // register the convert so it can be found + static var _ = hxd.fs.Convert.register(new ConvertPngToKtx2('png', 'ktx2')); +} #end From 8410650cea85ca20dcbaeb938c238cba5b9ca1ab Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Tue, 28 Jan 2025 15:03:30 +0000 Subject: [PATCH 06/12] fix: Only set AsyncLoading flag for ktx2 --- hxd/res/Image.hx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hxd/res/Image.hx b/hxd/res/Image.hx index c183811592..aa8ea9606f 100644 --- a/hxd/res/Image.hx +++ b/hxd/res/Image.hx @@ -620,9 +620,12 @@ class Image extends Resource { }); return; } - + switch (inf.dataFormat) { + case Ktx2ETC1S, Ktx2UASTC: + tex.flags.set(AsyncLoading); + default: + } function load() { - tex.flags.set(AsyncLoading); // TODO: Set when loading ktx2 if ((enableAsyncLoading || tex.flags.has(AsyncLoading)) && asyncData == null && asyncMessage == null && ASYNC_LOADER != null && ASYNC_LOADER.isSupported(this)) { tex.dispose(); From 8d68b3f6c7455295e29ded0ea6b9c83b8ae92c8d Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Tue, 18 Feb 2025 10:41:49 +0000 Subject: [PATCH 07/12] Cleanup and spelling --- h3d/impl/GlDriver.hx | 13 +++++++----- hxd/res/Ktx2.hx | 49 ++++---------------------------------------- 2 files changed, 12 insertions(+), 50 deletions(-) diff --git a/h3d/impl/GlDriver.hx b/h3d/impl/GlDriver.hx index e9fc4463dd..3949f46788 100644 --- a/h3d/impl/GlDriver.hx +++ b/h3d/impl/GlDriver.hx @@ -3,6 +3,8 @@ import h3d.impl.Driver; import h3d.mat.Pass; import h3d.mat.Stencil; import h3d.mat.Data; +import hxd.res.Ktx2.EngineFormat; +import hxd.res.Ktx2.InternalFormat; #if (js||hlsdl||usegl) @@ -1003,7 +1005,8 @@ class GlDriver extends Driver { case GL.RG, GL.RG8, GL.RG16F, GL.RG32F, 0x822C: GL.RG; case GL.RGB16F, GL.RGB32F, 0x8054, hxd.CompressedTextureFormat.BPTC_FORMAT.RGB_BPTC_UNSIGNED, hxd.CompressedTextureFormat.ETC_FORMAT.RGB_ETC1: GL.RGB; case 0x805B, hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT1,hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT3, - hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5,hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4, hxd.CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC : GL.RGBA; default: throw "Invalid format " + t.internalFmt; + hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5,hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4, hxd.CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC : GL.RGBA; + default: throw "Invalid format " + t.internalFmt; } } @@ -1031,7 +1034,7 @@ class GlDriver extends Driver { discardError(); var tt = gl.createTexture(); var bind = getBindType(t); - var tt : Texture = { t : tt, width : t.width, height : t.height, internalFmt : GL.RGBA, pixelFmt : GL.UNSIGNED_BYTE, bits : -1, bind : bind, bias : 0, startMip : t.startingMip #if multidriver, driver : this #end }; + var tt : Texture = { t : tt, width : t.width, height : t.height, internalFmt : GL.RGBA, pixelFmt : GL.UNSIGNED_BYTE, bits : -1, bind : bind, bias : 0, startMip : t.startingMip #if multidriver, driver : this #end }; switch( t.format ) { case RGBA: // default @@ -1145,7 +1148,7 @@ class GlDriver extends Driver { #if js // Modern texture allocation that supports both compressed and uncompressed texture in WebGL - // texStorate2D/3D is only defined in OpenGL 4.2 but is defined in openGL ES 3 which the js target targets + // texStorage2D/3D is only defined in OpenGL 4.2 but is defined in openGL ES 3 which the js target targets // Patch RGBA to be RGBA8 because texStorage expect a "Sized Internal Format" var sizedFormat = tt.internalFmt == GL.RGBA ? GL.RGBA8 : tt.internalFmt; @@ -1243,7 +1246,7 @@ class GlDriver extends Driver { } var defaultDepth : h3d.mat.Texture; - + override function getDefaultDepthBuffer() : h3d.mat.Texture { // Unfortunately there is no way to bind the depth buffer of the default frame buffer to a frame buffer object. if( defaultDepth != null ) @@ -1758,7 +1761,7 @@ class GlDriver extends Driver { throw "Invalid texture context"; #end gl.bindFramebuffer(GL.FRAMEBUFFER, commonFB); - + if( tex.flags.has(IsArray) ) gl.framebufferTextureLayer(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, tex.t.t, mipLevel, layer); else diff --git a/hxd/res/Ktx2.hx b/hxd/res/Ktx2.hx index 704789a14a..97d6ffe319 100644 --- a/hxd/res/Ktx2.hx +++ b/hxd/res/Ktx2.hx @@ -230,18 +230,6 @@ class Ktx2Decoder { static var _transcoderScript : String; static var _transcoderLoading : Promise<{ script : String, wasm : haxe.io.Bytes }>; - /** - Get transcoded texture - - @param bytes Texture data as BytesInput - @param cb Callback invoked when transcoding is done passing the transcoded texture. - **/ - public static function getTexture(bytes : haxe.io.BytesInput, cb : (texture : h3d.mat.Texture, header:KTX2Header) -> Void) { - getTranscodedData(bytes, (data, header) -> { - cb(createTexture(data, header), header); - }); - } - static function detectSupport() { #if js final driver : h3d.impl.GlDriver = cast h3d.Engine.getCurrent().driver; @@ -345,39 +333,6 @@ class Ktx2Decoder { }); } - static function createTexture( data : BasisWorkerMessageData, header:KTX2Header ) { - final create = (fmt:hxd.PixelFormat) -> { - if( header.faceCount > 1 || header.layerCount > 1 ) { - // TODO: Handle cube texture - throw 'Multi texture ktx2 files not supported'; - } - final face = data.faces[0]; - final mipmaps:Array = face.mipmaps; - final texture = new h3d.mat.Texture(data.width, data.height, null, fmt); - var level = 0; - for ( mipmap in mipmaps ) { - final bytes = haxe.io.Bytes.ofData(cast mipmap.data); - final pixels = new hxd.Pixels(mipmap.width, mipmap.height, bytes, fmt); - texture.uploadPixels(pixels, level); - level++; - } - if( mipmaps.length>1 ) { - texture.flags.set(MipMapped); - texture.mipMap = Linear; - } - texture; - } - final texture = switch (data.format) { - case EngineFormat.RGBA_ASTC_4x4_Format: create(hxd.PixelFormat.ASTC(10)); - case EngineFormat.RGBA_BPTC_Format: create(hxd.PixelFormat.S3TC(7)); - case EngineFormat.RGBA_S3TC_DXT5_Format: create(hxd.PixelFormat.S3TC(3)); - case EngineFormat.RGB_ETC1_Format: create(hxd.PixelFormat.ETC(0)); - case EngineFormat.RGBA_ETC2_EAC_Format: create(hxd.PixelFormat.ETC(1)); - default: - throw 'Ktx2Loader: No supported format available.'; - } - return texture; - } #if js static function initTranscoder() { _transcoderLoading = if( _transcoderLoading == null ) { @@ -910,6 +865,10 @@ class EngineType { public static final HalfFloatType = 1016; } +class InternalFormat { + public static final COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; +} + /** * Defines a mipmap level */ From 69a77553d31f9ede651b1bcb779fba9a5c8441c2 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Tue, 18 Feb 2025 10:43:20 +0000 Subject: [PATCH 08/12] Enable convert cases for compressed texture for js only --- hxd/Pixels.hx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hxd/Pixels.hx b/hxd/Pixels.hx index 4dc8799805..1a2379ab42 100644 --- a/hxd/Pixels.hx +++ b/hxd/Pixels.hx @@ -389,12 +389,11 @@ class Pixels { for( i in 0 ... this.width * this.height ) nbytes.setFloat(i << 2, this.bytes.getFloat(i << 4)); this.bytes = nbytes; - + #if js case [S3TC(a),S3TC(b)] if( a == b ): // nothing case [ASTC(a),ASTC(b)] if( a == b ): // Ktx2 will handle conversion case [ETC(a),ETC(b)] if( a == b ): // Ktx2 will handle conversion - case [RGBA,_]: // With ktx2/basis textures, temp RGBA texture will be assigned before transcoding - + #end #if (hl && hl_ver >= "1.10") case [S3TC(ver),_]: if( (width|height)&3 != 0 ) throw "Texture size should be 4x4 multiple"; From 1f777620779a9b2a27079c2ccde9eca79ef7b35a Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Tue, 18 Feb 2025 10:44:07 +0000 Subject: [PATCH 09/12] Use correct format when allocating pixels for compressed textures --- hxd/res/Image.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hxd/res/Image.hx b/hxd/res/Image.hx index aa8ea9606f..a7129ae337 100644 --- a/hxd/res/Image.hx +++ b/hxd/res/Image.hx @@ -336,7 +336,7 @@ class Image extends Resource { public static dynamic function customCheckInfo(i:Image) {} public function getPixels(?fmt:PixelFormat, ?index:Int) { - var pixels:hxd.Pixels; + var pixels:hxd.Pixels; if (index == null) index = 0; switch (getInfo().dataFormat) { @@ -633,7 +633,7 @@ class Image extends Resource { tex.flags.set(Loading); if(tex.format.match(S3TC(_) | ASTC(_) | ETC(_))){ - tex.uploadPixels(Pixels.alloc(inf.width, inf.height, RGBA)); + tex.uploadPixels(Pixels.alloc(inf.width, inf.height, tex.format)); } else { @:privateAccess tex.format = RGBA; @:privateAccess tex.width = 1; From abf536404c230ce226404f3e3d323af07ba5af59 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Mon, 24 Feb 2025 11:51:53 +0000 Subject: [PATCH 10/12] Use constants to set internalFmt --- h3d/impl/GlDriver.hx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/h3d/impl/GlDriver.hx b/h3d/impl/GlDriver.hx index 3949f46788..ee59991294 100644 --- a/h3d/impl/GlDriver.hx +++ b/h3d/impl/GlDriver.hx @@ -3,6 +3,7 @@ import h3d.impl.Driver; import h3d.mat.Pass; import h3d.mat.Stencil; import h3d.mat.Data; +import hxd.CompressedTextureFormat; import hxd.res.Ktx2.EngineFormat; import hxd.res.Ktx2.InternalFormat; @@ -1097,24 +1098,24 @@ class GlDriver extends Driver { case S3TC(n) if( n <= maxCompressedTexturesSupport ): checkMult4(t); switch( n ) { - case 1: tt.internalFmt = 0x83F1; // COMPRESSED_RGBA_S3TC_DXT1_EXT - case 2: tt.internalFmt = 0x83F2; // COMPRESSED_RGBA_S3TC_DXT3_EXT - case 3: tt.internalFmt = 0x83F3; // COMPRESSED_RGBA_S3TC_DXT5_EXT - case 6: tt.internalFmt = 0x8E8F; // COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT - case 7: tt.internalFmt = 0x8E8C; // COMPRESSED_RGBA_BPTC_UNORM + case 1: tt.internalFmt = DXT_FORMAT.RGBA_DXT1; + case 2: tt.internalFmt = DXT_FORMAT.RGBA_DXT3; + case 3: tt.internalFmt = DXT_FORMAT.RGBA_DXT5; + case 6: tt.internalFmt = BPTC_FORMAT.RGB_BPTC_UNSIGNED; + case 7: tt.internalFmt = BPTC_FORMAT.RGBA_BPTC; default: throw "Unsupported texture format "+t.format; } case ASTC(n): checkMult4(t); switch (n) { - case 10: tt.internalFmt = hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4; + case 10: tt.internalFmt = ASTC_FORMAT.RGBA_4x4; default: throw "Unsupported texture format " + t.format; } case ETC(n): checkMult4(t); switch (n) { - case 0: tt.internalFmt = hxd.CompressedTextureFormat.ETC_FORMAT.RGB_ETC1; - case 1: tt.internalFmt = hxd.CompressedTextureFormat.ETC_FORMAT.RGBA_ETC2; + case 0: tt.internalFmt = ETC_FORMAT.RGB_ETC1; + case 1: tt.internalFmt = ETC_FORMAT.RGBA_ETC2; } default: throw "Unsupported texture format "+t.format; From a8bf4b296c0bd0c632b0213b075fc41ea10a8996 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Mon, 24 Feb 2025 11:52:07 +0000 Subject: [PATCH 11/12] Fix indentation --- hxd/res/Image.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hxd/res/Image.hx b/hxd/res/Image.hx index a7129ae337..7ada3a7334 100644 --- a/hxd/res/Image.hx +++ b/hxd/res/Image.hx @@ -336,7 +336,7 @@ class Image extends Resource { public static dynamic function customCheckInfo(i:Image) {} public function getPixels(?fmt:PixelFormat, ?index:Int) { - var pixels:hxd.Pixels; + var pixels:hxd.Pixels; if (index == null) index = 0; switch (getInfo().dataFormat) { From 4d34a8ccfa9971d94970b3003c1821946569a1c1 Mon Sep 17 00:00:00 2001 From: Leo Bergman Date: Mon, 24 Feb 2025 11:52:36 +0000 Subject: [PATCH 12/12] Add functions for use with non-res ktx2 files --- hxd/res/Ktx2.hx | 620 ++++++++++++++++++++++++++++-------------------- 1 file changed, 364 insertions(+), 256 deletions(-) diff --git a/hxd/res/Ktx2.hx b/hxd/res/Ktx2.hx index 97d6ffe319..a9e0b01f78 100644 --- a/hxd/res/Ktx2.hx +++ b/hxd/res/Ktx2.hx @@ -1,6 +1,7 @@ package hxd.res; import haxe.io.UInt8Array; + using Lambda; #if js @@ -8,29 +9,33 @@ typedef Promise = js.lib.Promise; typedef ImageData = js.html.ImageData; typedef Uint8Array = js.lib.Uint8Array; typedef Worker = js.html.Worker; -#else +#else // TODO: Add support for native targets, just dummy typing for now... class Promise { public function new(cb:(resolve:T, reject:T) -> Void) {} + public function then(cb:(message:T) -> Void) {}; + static function resolve(thenable:Dynamic):Promise { return null; }; + static function reject(?reason:Dynamic):Promise { return null; - }; -} +} + typedef ImageData = Dynamic; typedef Uint8Array = UInt8Array; + class Worker { public var index:Int; - public function new(url:String) { - - } + + public function new(url:String) {} public function postMessage(message:Dynamic, ?transfer:Array) {} - dynamic public function onmessage(e : { data : { type : String, id : Int } }) {}; + + dynamic public function onmessage(e:{data:{type:String, id:Int}}) {}; } #end @@ -47,11 +52,11 @@ class Ktx2 { @return Parsed ktx2 file **/ - public static function readFile( bytes : haxe.io.BytesInput ) : Ktx2File { + public static function readFile(bytes:haxe.io.BytesInput):Ktx2File { final header = readHeader(bytes); final levels = readLevels(bytes, header.levelCount); final dfd = readDfd(bytes); - final file : Ktx2File = { + final file:Ktx2File = { header: header, levels: levels, dfd: dfd, @@ -61,18 +66,29 @@ class Ktx2 { return file; } - public static function readHeader( bytes : haxe.io.BytesInput) : KTX2Header { + public static function readHeader(bytes:haxe.io.BytesInput):KTX2Header { final ktx2Id = [ // '´', 'K', 'T', 'X', '2', '0', 'ª', '\r', '\n', '\x1A', '\n' - 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A, + 0xAB, + 0x4B, + 0x54, + 0x58, + 0x20, + 0x32, + 0x30, + 0xBB, + 0x0D, + 0x0A, + 0x1A, + 0x0A, ]; - + final matching = ktx2Id.mapi((i, id) -> id == bytes.readByte()); - - if( matching.contains(false) ) { + + if (matching.contains(false)) { throw 'Invalid KTX2 header'; } - final header :KTX2Header = { + final header:KTX2Header = { vkFormat: bytes.readInt32(), typeSize: bytes.readInt32(), pixelWidth: bytes.readInt32(), @@ -89,57 +105,57 @@ class Ktx2 { kvdByteLength: bytes.readInt32(), sgdByteOffset: { final val = bytes.read(8).getInt64(0); - if( val.high>0 ) { + if (val.high > 0) { throw BYTE_INDEX_ERROR; } val.low; - }, + }, sgdByteLength: { final val = bytes.read(8).getInt64(0); - if( val.high>0 ) { + if (val.high > 0) { throw BYTE_INDEX_ERROR; } val.low; } } - if( header.pixelDepth>0 ) { + if (header.pixelDepth > 0) { throw 'Failed to parse KTX2 file - Only 2D textures are currently supported.'; } - if( header.layerCount>1 ) { - throw 'Failed to parse KTX2 file - Array textures are not currently supported.'; + if (header.layerCount > 1) { + throw 'Failed to parse KTX2 file - Array textures are not currently supported.'; } - if( header.faceCount>1 ) { - throw 'Failed to parse KTX2 file - Cube textures are not currently supported.'; + if (header.faceCount > 1) { + throw 'Failed to parse KTX2 file - Cube textures are not currently supported.'; } return header; } - static function readLevels( bytes : haxe.io.BytesInput, levelCount : Int) : Array { + static function readLevels(bytes:haxe.io.BytesInput, levelCount:Int):Array { levelCount = hxd.Math.imax(1, levelCount); final length = levelCount * 3 * (2 * 4); final level = bytes.read(length); final levels:Array = []; - - while( levelCount-- > 0 ) { + + while (levelCount-- > 0) { levels.push({ byteOffset: { final val = level.getInt64(0); - if( val.high>0 ) { + if (val.high > 0) { throw BYTE_INDEX_ERROR; } val.low; }, byteLength: { final val = level.getInt64(8); - if( val.high>0 ) { + if (val.high > 0) { throw BYTE_INDEX_ERROR; } val.low; }, uncompressedByteLength: { final val = level.getInt64(16); - if( val.high>0 ) { + if (val.high > 0) { throw BYTE_INDEX_ERROR; } val.low; @@ -149,15 +165,15 @@ class Ktx2 { return levels; } - static function readDfd( bytes : haxe.io.BytesInput) : KTX2DFD { + static function readDfd(bytes:haxe.io.BytesInput):KTX2DFD { final totalSize = bytes.readInt32(); final vendorId = bytes.readInt16(); final descriptorType = bytes.readInt16(); final versionNumber = bytes.readInt16(); final descriptorBlockSize = bytes.readInt16(); - final numSamples = Std.int((descriptorBlockSize-24)/16); - final dfdBlock : KTX2DFD = { - vendorId:vendorId, + final numSamples = Std.int((descriptorBlockSize - 24) / 16); + final dfdBlock:KTX2DFD = { + vendorId: vendorId, descriptorType: descriptorType, versionNumber: versionNumber, descriptorBlockSize: descriptorBlockSize, @@ -166,10 +182,10 @@ class Ktx2 { transferFunction: bytes.readByte(), flags: bytes.readByte(), texelBlockDimension: { - x: bytes.readByte()+1, - y: bytes.readByte()+1, - z: bytes.readByte()+1, - w: bytes.readByte()+1, + x: bytes.readByte() + 1, + y: bytes.readByte() + 1, + z: bytes.readByte() + 1, + w: bytes.readByte() + 1, }, bytesPlane: [ bytes.readByte() /* bytesPlane0 */, @@ -183,20 +199,20 @@ class Ktx2 { ], numSamples: numSamples, samples: [ - for( i in 0...numSamples ) { + for (i in 0...numSamples) { final bitOffset = bytes.readUInt16(); - final bitLength = bytes.readByte()+1; + final bitLength = bytes.readByte() + 1; final channelType = bytes.readByte(); - final channelFlags = (channelType & 0xf0)>>4; - final samplePosition = [ + final channelFlags = (channelType & 0xf0) >> 4; + final samplePosition = [ bytes.readByte() /* samplePosition0 */, bytes.readByte() /* samplePosition1 */, bytes.readByte() /* samplePosition2 */, bytes.readByte() /* samplePosition3 */, ]; - final sampleLower = bytes.readUInt16()+bytes.readUInt16(); - final sampleUpper = bytes.readUInt16()+bytes.readUInt16(); - final sample : KTX2Sample = { + final sampleLower = bytes.readUInt16() + bytes.readUInt16(); + final sampleUpper = bytes.readUInt16() + bytes.readUInt16(); + final sample:KTX2Sample = { bitOffset: bitOffset, bitLength: bitLength, channelType: channelType & 0x0F, @@ -213,98 +229,56 @@ class Ktx2 { } } - /** Handles transcoding of Ktx2 textures **/ class Ktx2Decoder { - public static var mscTranscoder : Dynamic; + public static var mscTranscoder:Dynamic; public static var workerLimit = 4; static var _workerNextTaskID = 1; - static var _workerSourceURL : String; - static var _workerConfig : BasisWorkerConfig; - static var _workerPool : Array = []; - static var _transcoderPending : Promise; - static var _transcoderBinary : haxe.io.Bytes; - static var _transcoderScript : String; - static var _transcoderLoading : Promise<{ script : String, wasm : haxe.io.Bytes }>; + static var _workerSourceURL:String; + static var _workerConfig:BasisWorkerConfig; + static var _workerPool:Array = []; + static var _transcoderPending:Promise; + static var _transcoderBinary:haxe.io.Bytes; + static var _transcoderScript:String; + static var _transcoderLoading:Promise<{script:String, wasm:haxe.io.Bytes}>; - static function detectSupport() { - #if js - final driver : h3d.impl.GlDriver = cast h3d.Engine.getCurrent().driver; - return { - astcSupported: driver.textureSupport.astc, - etc1Supported: driver.textureSupport.etc1, - etc2Supported: driver.textureSupport.etc2, - dxtSupported: driver.textureSupport.dxt, - bptcSupported: driver.textureSupport.bptc, - } - #else - return null; - #end - } + /** + Transcode and get the texture data. - static function getWorker():Promise { - return initTranscoder().then(val -> { - if( _workerPool.length < workerLimit ) { - final worker = new Worker(_workerSourceURL); - final workerTask : WorkerTask = { - worker: worker, - callbacks: new haxe.ds.IntMap(), - taskCosts: new haxe.ds.IntMap(), - taskLoad: 0, - } - worker.postMessage({ - type: 'init', - config: _workerConfig, - transcoderBinary: _transcoderBinary, - }); - - worker.onmessage = e -> { - var message = e.data; - switch( message.type ) { - case 'transcode': - workerTask.callbacks.get(message.id).resolve(message); - case 'error': - workerTask.callbacks.get(message.id).reject(message); - default: - throw 'Ktx2Loader: Unexpected message, "${message.type}"'; - } - }; - _workerPool.push(workerTask); - } else { - _workerPool.sort((a, b) -> a.taskLoad > b.taskLoad ? -1 : 1); - } - - return _workerPool[_workerPool.length-1]; - }); - } + @param bytes Texture data as BytesInput + @param cb Callback invoked when transcoding is done, passing the transcoded texture data. + **/ public static function getTranscodedData(buffer:haxe.io.BytesInput, cb:(data:BasisWorkerMessageData, header:KTX2Header) -> Void) { _workerConfig ??= detectSupport(); - if ( _workerConfig == null ) { + if (_workerConfig == null) { throw "Not implemented: Ktx2 only supported on js target"; } - + final ktx2File = Ktx2.readFile(buffer); - // Determine basis format - final basisFormat = if ( ktx2File.header.vkFormat == 0 ) { - if ( ktx2File.dfd.colorModel == Ktx2.DFDModel.ETC1S ) ETC1S - else if ( ktx2File.dfd.colorModel == Ktx2.DFDModel.UASTC ) UASTC - else if ( ktx2File.dfd.transferFunction == Ktx2.DFDTransferFunction.LINEAR ) UASTC_HDR - else throw "KTX2Loader: Unknown Basis encoding"; + final basisFormat = if (ktx2File.header.vkFormat == 0) { + if (ktx2File.dfd.colorModel == Ktx2.DFDModel.ETC1S) + ETC1S + else if (ktx2File.dfd.colorModel == Ktx2.DFDModel.UASTC) + UASTC + else if (ktx2File.dfd.transferFunction == Ktx2.DFDTransferFunction.LINEAR) + UASTC_HDR + else + throw "KTX2Loader: Unknown Basis encoding"; } else { throw "KTX2Loader: Non-zero vkFormat not supported"; }; - + // Get transcoder format final formatInfo = getTranscoderFormat(basisFormat, ktx2File.header.pixelWidth, ktx2File.header.pixelHeight, ktx2File.dfd.hasAlpha()); getWorker().then((task:WorkerTask) -> { final worker = task.worker; final taskID = _workerNextTaskID++; - + final textureDone = new Promise((resolve, reject) -> { task.callbacks.set(taskID, { resolve: resolve, @@ -315,13 +289,13 @@ class Ktx2Decoder { buffer.position = 0; final bytes = buffer.readAll().getData(); worker.postMessage({ - type: 'transcode', - id: taskID, - buffer: bytes, + type: 'transcode', + id: taskID, + buffer: bytes, formatInfo: formatInfo, }, [bytes]); }); - + textureDone.then((message:BasisWorkerMessage) -> { if (message.type == 'error') { throw 'Unable to decode ktx2 file: ${message.error}'; @@ -333,9 +307,110 @@ class Ktx2Decoder { }); } + static function detectSupport() { + #if js + final driver:h3d.impl.GlDriver = cast h3d.Engine.getCurrent().driver; + return { + astcSupported: driver.textureSupport.astc, + etc1Supported: driver.textureSupport.etc1, + etc2Supported: driver.textureSupport.etc2, + dxtSupported: driver.textureSupport.dxt, + bptcSupported: driver.textureSupport.bptc, + } + #else + return null; + #end + } + + #if non_res_ktx2 + /** + Get transcoded texture. + + Used in combination with "non_res_ktx2" flag when handling loading of ktx2 outside of heaps res system. + + @param bytes Texture data as BytesInput + @param cb Callback invoked when transcoding is done, passing the transcoded texture. + **/ + public static function getTexture(bytes:haxe.io.BytesInput, cb:(texture:h3d.mat.Texture, header:KTX2Header) -> Void) { + getTranscodedData(bytes, (data, header) -> { + cb(createTexture(data, header), header); + }); + } + + static function createTexture(data:BasisWorkerMessageData, header:KTX2Header) { + final create = (fmt:hxd.PixelFormat) -> { + if (header.faceCount > 1 || header.layerCount > 1) { + // TODO: Handle cube texture + throw 'Multi texture ktx2 files not supported'; + } + final face = data.faces[0]; + final mipmaps:Array = face.mipmaps; + final texture = new h3d.mat.Texture(data.width, data.height, null, fmt); + var level = 0; + for (mipmap in mipmaps) { + final bytes = haxe.io.Bytes.ofData(cast mipmap.data); + final pixels = new hxd.Pixels(mipmap.width, mipmap.height, bytes, fmt); + texture.uploadPixels(pixels, level); + level++; + } + if (mipmaps.length > 1) { + texture.flags.set(MipMapped); + texture.mipMap = Linear; + } + texture; + } + final texture = switch (data.format) { + case EngineFormat.RGBA_ASTC_4x4_Format: create(hxd.PixelFormat.ASTC(10)); + case EngineFormat.RGBA_BPTC_Format: create(hxd.PixelFormat.S3TC(7)); + case EngineFormat.RGBA_S3TC_DXT5_Format: create(hxd.PixelFormat.S3TC(3)); + case EngineFormat.RGB_ETC1_Format: create(hxd.PixelFormat.ETC(0)); + case EngineFormat.RGBA_ETC2_EAC_Format: create(hxd.PixelFormat.ETC(1)); + default: + throw 'Ktx2Loader: No supported format available.'; + } + return texture; + } + #end + + static function getWorker():Promise { + return initTranscoder().then(val -> { + if (_workerPool.length < workerLimit) { + final worker = new Worker(_workerSourceURL); + final workerTask:WorkerTask = { + worker: worker, + callbacks: new haxe.ds.IntMap(), + taskCosts: new haxe.ds.IntMap(), + taskLoad: 0, + } + worker.postMessage({ + type: 'init', + config: _workerConfig, + transcoderBinary: _transcoderBinary, + }); + + worker.onmessage = e -> { + var message = e.data; + switch (message.type) { + case 'transcode': + workerTask.callbacks.get(message.id).resolve(message); + case 'error': + workerTask.callbacks.get(message.id).reject(message); + default: + throw 'Ktx2Loader: Unexpected message, "${message.type}"'; + } + }; + _workerPool.push(workerTask); + } else { + _workerPool.sort((a, b) -> a.taskLoad > b.taskLoad ? -1 : 1); + } + + return _workerPool[_workerPool.length - 1]; + }); + } + #if js static function initTranscoder() { - _transcoderLoading = if( _transcoderLoading == null ) { + _transcoderLoading = if (_transcoderLoading == null) { // Load transcoder wrapper. final jsLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.js'); final jsContent = new Promise((resolve, reject) -> { @@ -350,11 +425,11 @@ class Ktx2Decoder { binaryLoader.onError = reject; binaryLoader.load(true); }); - Promise.all([jsContent, binaryContent]).then(arr -> {script:arr[0].toString(), wasm:arr[1]}); + Promise.all([jsContent, binaryContent]).then(arr -> {script: arr[0].toString(), wasm: arr[1]}); } else { _transcoderLoading; } - + _transcoderPending = _transcoderLoading.then(o -> { _transcoderScript = o.script; _transcoderBinary = o.wasm; @@ -494,8 +569,8 @@ class Ktx2Decoder { } '; - final blob = new js.html.Blob([workerScript], {type: "text/javascript"}); - _workerSourceURL = js.Syntax.code("URL.createObjectURL({0})", blob); + final blob = new js.html.Blob([workerScript], {type: "text/javascript"}); + _workerSourceURL = js.Syntax.code("URL.createObjectURL({0})", blob); }); return _transcoderPending; } @@ -504,58 +579,59 @@ class Ktx2Decoder { return null; } #end - public static function getTranscoderFormat(basisFormat:BasisFormat, width:Int, height:Int, hasAlpha:Bool):{ transcoderFormat:Int, engineFormat:Int, engineType:Int } { - _workerConfig ??= detectSupport(); + + public static function getTranscoderFormat(basisFormat:BasisFormat, width:Int, height:Int, + hasAlpha:Bool):{transcoderFormat:Int, engineFormat:Int, engineType:Int} { + _workerConfig ??= detectSupport(); final caps = _workerConfig; return switch basisFormat { - case BasisFormat.ETC1S if(hasAlpha && caps.etc2Supported): - { transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.ETC1S if(!hasAlpha && (caps.etc1Supported || caps.etc2Supported)): - { transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.ETC1S if(caps.bptcSupported): - { transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.ETC1S if(hasAlpha && caps.dxtSupported): - { transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.ETC1S if(!hasAlpha && caps.dxtSupported): - { transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.ETC1S: - { transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType }; - - case BasisFormat.UASTC if(caps.astcSupported): - { transcoderFormat: TranscoderFormat.ASTC_4x4, engineFormat: EngineFormat.RGBA_ASTC_4x4_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC if(caps.bptcSupported): - { transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC if(hasAlpha && caps.etc2Supported): - { transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC if((!hasAlpha && caps.etc2Supported) || caps.etc1Supported): - { transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC if(hasAlpha && caps.dxtSupported): - { transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC if(!hasAlpha && caps.dxtSupported): - { transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType }; - case BasisFormat.UASTC: - { transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType }; - - case BasisFormat.UASTC_HDR: - { transcoderFormat: TranscoderFormat.RGBA_HALF, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.HalfFloatType }; - - case _: - throw 'KTX2Loader: Failed to identify transcoding target.'; - } - } - - static function isPowerOfTwo(value:Int):Bool { - return value > 0 && (value & (value - 1)) == 0; - } - + case BasisFormat.ETC1S if (hasAlpha && caps.etc2Supported): + {transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.ETC1S if (!hasAlpha && (caps.etc1Supported || caps.etc2Supported)): + {transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.ETC1S if (caps.bptcSupported): + {transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.ETC1S if (hasAlpha && caps.dxtSupported): + {transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.ETC1S if (!hasAlpha && caps.dxtSupported): + {transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.ETC1S: + {transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType}; + + case BasisFormat.UASTC if (caps.astcSupported): + {transcoderFormat: TranscoderFormat.ASTC_4x4, engineFormat: EngineFormat.RGBA_ASTC_4x4_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC if (caps.bptcSupported): + {transcoderFormat: TranscoderFormat.BC7_M5, engineFormat: EngineFormat.RGBA_BPTC_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC if (hasAlpha && caps.etc2Supported): + {transcoderFormat: TranscoderFormat.ETC2, engineFormat: EngineFormat.RGBA_ETC2_EAC_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC if ((!hasAlpha && caps.etc2Supported) || caps.etc1Supported): + {transcoderFormat: TranscoderFormat.ETC1, engineFormat: EngineFormat.RGB_ETC1_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC if (hasAlpha && caps.dxtSupported): + {transcoderFormat: TranscoderFormat.BC3, engineFormat: EngineFormat.RGBA_S3TC_DXT5_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC if (!hasAlpha && caps.dxtSupported): + {transcoderFormat: TranscoderFormat.BC1, engineFormat: EngineFormat.RGB_S3TC_DXT1_Format, engineType: EngineType.UnsignedByteType}; + case BasisFormat.UASTC: + {transcoderFormat: TranscoderFormat.RGBA32, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.UnsignedByteType}; + + case BasisFormat.UASTC_HDR: + {transcoderFormat: TranscoderFormat.RGBA_HALF, engineFormat: EngineFormat.RGBAFormat, engineType: EngineType.HalfFloatType}; + + case _: + throw 'KTX2Loader: Failed to identify transcoding target.'; + } + } + + static function isPowerOfTwo(value:Int):Bool { + return value > 0 && (value & (value - 1)) == 0; + } } typedef Ktx2File = { - header : KTX2Header, - levels : Array, - dfd : KTX2DFD, - data : Uint8Array, - supercompressionGlobalData : KTX2SupercompressionGlobalData, + header:KTX2Header, + levels:Array, + dfd:KTX2DFD, + data:Uint8Array, + supercompressionGlobalData:KTX2SupercompressionGlobalData, } enum abstract SuperCompressionScheme(Int) from Int to Int { @@ -605,67 +681,81 @@ enum abstract SupercompressionScheme(Int) from Int to Int { /** Vulkan format. Will be 0 for universal texture formats. **/ - public final vkFormat : Int; + public final vkFormat:Int; + /** Size of data type in bytes used to upload data to a graphics API. **/ - public final typeSize : Int; + public final typeSize:Int; + /** The width of texture image for level 0, in pixels. **/ - public final pixelWidth : Int; + public final pixelWidth:Int; + /** The height of texture image for level 0, in pixels. **/ - public final pixelHeight : Int; + public final pixelHeight:Int; + /** The depth of texture image for level 0, in pixels. **/ - public final pixelDepth : Int; + public final pixelDepth:Int; + /** Number of array elements. If texture is not an array texture, layerCount must equal 0. **/ - public final layerCount : Int; + public final layerCount:Int; + /** Number of cubemap faces. For cubemaps and cubemap arrays this must be 6. For non cubemaps this must be 1. **/ - public final faceCount : Int; + public final faceCount:Int; + /** Specifies number of mip levels. **/ - public final levelCount : Int; + public final levelCount:Int; + /*** Indicates if supercompression scheme has been applied. 0=None, 1=BasisLZ, 2=Zstandard, 3=ZLIB **/ - public final supercompressionScheme : Int; + public final supercompressionScheme:Int; + /** Offset from start of file for dfdTotalSize field in Data Format Descriptor **/ - public final dfdByteOffset : Int; + public final dfdByteOffset:Int; + /** Total number of bytes in the Data Format Descriptor, including dfdTotalSize field. **/ - public final dfdByteLength : Int; + public final dfdByteLength:Int; + /** Offset of key/value pair data **/ - public final kvdByteOffset : Int; + public final kvdByteOffset:Int; + /** Total number of bytes of key/value data **/ - public final kvdByteLength : Int; + public final kvdByteLength:Int; + /** The offset from the start of the file of supercompressionGlobalData. **/ - public final sgdByteOffset : Int; + public final sgdByteOffset:Int; + /** Number of bytes of supercompressionGlobalData. **/ - public final sgdByteLength : Int; + public final sgdByteLength:Int; public function needZSTDDecoder() { return supercompressionScheme == SupercompressionScheme.ZStandard; - } + } } typedef KTX2Level = { @@ -673,76 +763,88 @@ typedef KTX2Level = { Byte offset. According to spec this should be 64 bit, but since a lot of byte code in haxe is using regular 32 bit Int for indexing, supporting files too large to fit in 32bit space is complicated and should not be needed for individual game assets. **/ - final byteOffset : Int; - final byteLength : Int; - final uncompressedByteLength : Int; + final byteOffset:Int; + + final byteLength:Int; + final uncompressedByteLength:Int; } typedef KTX2Sample = { - final bitOffset : Int; - final bitLength : Int; - final channelType : Int; - final channelFlags : Int; - final samplePosition : Array; - final sampleLower : Int; - final sampleUpper : Int; + final bitOffset:Int; + final bitLength:Int; + final channelType:Int; + final channelFlags:Int; + final samplePosition:Array; + final sampleLower:Int; + final sampleUpper:Int; } /** Ktx2 Document Format Description */ -@:structInit class KTX2DFD { +@:structInit class KTX2DFD { /** Defined as 0 in spec **/ - public final vendorId : Int; + public final vendorId:Int; + /** Defined as 0 in spec **/ - public final descriptorType : Int; + public final descriptorType:Int; + /** Defined as 2 in spec for ktx2 **/ - public final versionNumber : Int; + public final versionNumber:Int; + /** Size in bytes of this Descriptor Block **/ - public final descriptorBlockSize : Int; + public final descriptorBlockSize:Int; + /** Color model for encoded data (ETC1S=163, UASTC=166) **/ - public final colorModel : Int; + public final colorModel:Int; + /** Color primaries used when encoding. BT709/SRGB (1) recommended for standard dynamic range, standard gamut images. See KHR_DF_PRIMARIES in khr_df.h for other values. **/ - public final colorPrimaries : Int; + public final colorPrimaries:Int; + /** Encoding curve used to map luminance, with values like KHR_DF_TRANSFER_LINEAR, KHR_DF_TRANSFER_SRGB, or KHR_DF_TRANSFER_ST2084 for HDR. See KHR_DF_TRANSFER in khr_df.h for other values. **/ - public final transferFunction : Int; + public final transferFunction:Int; + /** Indicates if premultiplied aplha should be used. KHR_DF_FLAG_ALPHA_PREMULTIPLIED (1) for PMA, or KHR_DF_FLAG_ALPHA_STRAIGHT (0) for non-PMA. **/ - public final flags : Int; + public final flags:Int; + /** - Integer bound on range of coordinates covered by repeating block described by samples. Four separate values, represented as unsigned 8-bit integers, are supported, corresponding to successive dimensions. + Integer bound on range of coordinates covered by repeating block described by samples. Four separate values, represented as unsigned 8-bit integers, are supported, corresponding to successive dimensions. **/ - public final texelBlockDimension : { - x : Int, - y : Int, - z : Int, - w : Int, + public final texelBlockDimension:{ + x:Int, + y:Int, + z:Int, + w:Int, }; + /** Number of bytes which a plane contributes to the format. **/ - public final bytesPlane : Array; + public final bytesPlane:Array; + /** Number of samples present in the format. **/ - public final numSamples : Int; + public final numSamples:Int; + /** Samples data **/ - public final samples : Array; + public final samples:Array; /** Check if texture data has alpha channel @@ -751,9 +853,9 @@ typedef KTX2Sample = { **/ public function hasAlpha() { return switch colorModel { - case hxd.res.Ktx2.DFDModel.ETC1S: - numSamples == 2 && (samples[0].channelType == DFDChannel_ETC1S.AAA || samples[1].channelType == DFDChannel_ETC1S.AAA); - case hxd.res.Ktx2.DFDModel.UASTC: + case hxd.res.Ktx2.DFDModel.ETC1S: numSamples == 2 && (samples[0].channelType == DFDChannel_ETC1S.AAA + || samples[1].channelType == DFDChannel_ETC1S.AAA); + case hxd.res.Ktx2.DFDModel.UASTC: samples[0].channelType == DFDChannel_UASTC.RGBA; default: throw 'Unsupported colorModel in ktx2 file ${colorModel}'; } @@ -770,25 +872,25 @@ typedef KTX2Sample = { } typedef KTX2ImageDesc = { - final imageFlags : Int; - final rgbSliceByteOffset : Int; - final rgbSliceByteLength : Int; - final alphaSliceByteOffset : Int; - final alphaSliceByteLength : Int; + final imageFlags:Int; + final rgbSliceByteOffset:Int; + final rgbSliceByteLength:Int; + final alphaSliceByteOffset:Int; + final alphaSliceByteLength:Int; } typedef KTX2SupercompressionGlobalData = { - final endpointCount : Int; - final selectorCount : Int; - final endpointsByteLength : Int; - final selectorsByteLength : Int; - final tablesByteLength : Int; - final extendedByteLength : Int; - final imageDescs : Array; - final endpointsData : haxe.io.UInt8Array; - final selectorsData : haxe.io.UInt8Array; - final tablesData : haxe.io.UInt8Array; - final extendedData : haxe.io.UInt8Array; + final endpointCount:Int; + final selectorCount:Int; + final endpointsByteLength:Int; + final selectorsByteLength:Int; + final tablesByteLength:Int; + final extendedByteLength:Int; + final imageDescs:Array; + final endpointsData:haxe.io.UInt8Array; + final selectorsData:haxe.io.UInt8Array; + final tablesData:haxe.io.UInt8Array; + final extendedData:haxe.io.UInt8Array; } @:keep @@ -845,15 +947,15 @@ enum BasisFormat { class EngineFormat { public static final RGBAFormat = 0x03FF; public static final RGBA8Format = 0x8058; - public static final R8Format = 0x8229; - public static final RG8Format = 0x822b; + public static final R8Format = 0x8229; + public static final RG8Format = 0x822b; public static final RGBA_ASTC_4x4_Format = CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4; public static final RGB_BPTC_UNSIGNED_Format = CompressedTextureFormat.BPTC_FORMAT.RGB_BPTC_UNSIGNED; public static final RGBA_BPTC_Format = CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC; public static final RGB_S3TC_DXT1_Format = CompressedTextureFormat.DXT_FORMAT.RGB_DXT1; public static final RGBA_S3TC_DXT1_Format = CompressedTextureFormat.DXT_FORMAT.RGBA_DXT1; public static final RGBA_S3TC_DXT5_Format = CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5; - public static final RGB_ETC1_Format = CompressedTextureFormat.ETC_FORMAT.RGB_ETC1; + public static final RGB_ETC1_Format = CompressedTextureFormat.ETC_FORMAT.RGB_ETC1; public static final RGBA_ETC2_EAC_Format = CompressedTextureFormat.ETC_FORMAT.RGBA_ETC2; public static final RGB_ETC2_Format = 0x9274; } @@ -876,47 +978,53 @@ class InternalFormat { /** * The data of the mipmap level */ - public var data : Null = null; + public var data:Null = null; /** * The width of the mipmap level */ - public final width : Int; + public final width:Int; /** * The height of the mipmap level */ - public final height : Int; + public final height:Int; } typedef WorkerTask = { - worker : Worker, - callbacks : haxe.ds.IntMap<{ resolve : (value : Dynamic) -> Void, reject : (reason : Dynamic) -> Void }>, - taskCosts : haxe.ds.IntMap, - taskLoad : Int, + worker:Worker, + callbacks:haxe.ds.IntMap<{resolve:(value:Dynamic) -> Void, reject:(reason:Dynamic) -> Void}>, + taskCosts:haxe.ds.IntMap, + taskLoad:Int, } typedef BasisWorkerConfig = { - astcSupported : Bool, - etc1Supported : Bool, - etc2Supported : Bool, - dxtSupported : Bool, - bptcSupported : Bool, + astcSupported:Bool, + etc1Supported:Bool, + etc2Supported:Bool, + dxtSupported:Bool, + bptcSupported:Bool, } @:structInit class BasisWorkerMessage { - public final id : String; + public final id:String; public final type = 'transcode'; - public final data : BasisWorkerMessageData; - public final error : String = null; + public final data:BasisWorkerMessageData; + public final error:String = null; } typedef BasisWorkerMessageData = { - faces : Array<{ mipmaps : Array, width : Int, height : Int, format : Int, type : Int }>, - width : Int, - height : Int, - hasAlpha : Bool, - format : Int, - type : Int, - dfdFlags : Int, + faces:Array<{ + mipmaps:Array, + width:Int, + height:Int, + format:Int, + type:Int + }>, + width:Int, + height:Int, + hasAlpha:Bool, + format:Int, + type:Int, + dfdFlags:Int, }