diff --git a/h3d/impl/GlDriver.hx b/h3d/impl/GlDriver.hx index d771357b4d..ee59991294 100644 --- a/h3d/impl/GlDriver.hx +++ b/h3d/impl/GlDriver.hx @@ -3,6 +3,9 @@ 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; #if (js||hlsdl||usegl) @@ -87,6 +90,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,8 +1004,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; + 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; } } @@ -1022,7 +1035,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 @@ -1083,16 +1096,27 @@ 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 - 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 = ASTC_FORMAT.RGBA_4x4; + default: throw "Unsupported texture format " + t.format; + } + case ETC(n): + checkMult4(t); + switch (n) { + case 0: tt.internalFmt = ETC_FORMAT.RGB_ETC1; + case 1: tt.internalFmt = ETC_FORMAT.RGBA_ETC2; + } default: throw "Unsupported texture format "+t.format; } @@ -1125,7 +1149,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; @@ -1169,6 +1193,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 ) @@ -1218,7 +1247,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 ) @@ -1409,7 +1438,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 @@ -1733,7 +1762,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 @@ -1893,20 +1922,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 +2172,4 @@ class GlDriver extends Driver { } -#end +#end \ No newline at end of file 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..41f0fbe491 100644 --- a/hxd/PixelFormat.hx +++ b/hxd/PixelFormat.hx @@ -23,9 +23,28 @@ enum PixelFormat { RG16U; RGB16U; RGBA16U; - S3TC( v : Int ); + /** + 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; Depth24Stencil8; Depth32; -} \ No newline at end of file +} diff --git a/hxd/Pixels.hx b/hxd/Pixels.hx index b56a0f75da..1a2379ab42 100644 --- a/hxd/Pixels.hx +++ b/hxd/Pixels.hx @@ -389,10 +389,11 @@ class Pixels { for( i in 0 ... this.width * this.height ) nbytes.setFloat(i << 2, this.bytes.getFloat(i << 4)); this.bytes = nbytes; - - case [S3TC(a),S3TC(b)] if( a == b ): - // nothing - + #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 + #end #if (hl && hl_ver >= "1.10") case [S3TC(ver),_]: if( (width|height)&3 != 0 ) throw "Texture size should be 4x4 multiple"; @@ -405,7 +406,6 @@ class Pixels { convert(target); return; #end - default: throw "Cannot convert from " + format + " to " + target; } @@ -536,8 +536,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 +584,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 +645,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/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 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/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..7ada3a7334 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,6 +12,8 @@ enum abstract ImageFormat(Int) { var Dds = 4; var Raw = 5; var Hdr = 6; + 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. @@ -35,6 +41,8 @@ enum abstract ImageFormat(Int) { case Dds: "DDS"; case Raw: "RAW"; case Hdr: "HDR"; + case Ktx2ETC1S: "KTX2ETC1S"; + case Ktx2UASTC: "KTX2UASTC"; }; } } @@ -242,6 +250,36 @@ 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)); + 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 = 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 inf.dataFormat = Hdr; @@ -435,6 +473,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 Ktx2ETC1S, Ktx2UASTC: + var bytes = entry.getBytes(); + pixels = new hxd.Pixels(inf.width, inf.height, bytes, inf.pixelFormat); } if (fmt != null) pixels.convert(fmt); @@ -530,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); @@ -563,20 +620,28 @@ class Image extends Resource { }); return; } - + switch (inf.dataFormat) { + case Ktx2ETC1S, Ktx2UASTC: + tex.flags.set(AsyncLoading); + default: + } function load() { - if ((enableAsyncLoading || tex.flags.has(AsyncLoading)) && asyncData == null && ASYNC_LOADER != null && ASYNC_LOADER.isSupported(this)) - @:privateAccess { + 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, tex.format)); + } 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; @@ -586,7 +651,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; @@ -610,14 +675,30 @@ class Image extends Resource { pos += size; } } - 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 new file mode 100644 index 0000000000..a9e0b01f78 --- /dev/null +++ b/hxd/res/Ktx2.hx @@ -0,0 +1,1030 @@ +package hxd.res; + +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. +**/ +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 Uint8Array(cast @: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; + } +} + +/** + Handles transcoding of Ktx2 textures +**/ +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:Promise; + static var _transcoderBinary:haxe.io.Bytes; + static var _transcoderScript:String; + static var _transcoderLoading:Promise<{script:String, wasm:haxe.io.Bytes}>; + + /** + Transcode and get the texture data. + + @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) { + 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"; + } 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, + 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, + formatInfo: formatInfo, + }, [bytes]); + }); + + textureDone.then((message:BasisWorkerMessage) -> { + if (message.type == 'error') { + throw 'Unable to decode ktx2 file: ${message.error}'; + } + buffer.position = 0; + final header = Ktx2.readFile(buffer).header; + cb(message.data, header); + }); + }); + } + + 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) { + // Load transcoder wrapper. + final jsLoader = new hxd.net.BinaryLoader('vendor/basis_transcoder.js'); + 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 Promise((resolve, reject) -> { + binaryLoader.onLoaded = resolve; + binaryLoader.onError = reject; + binaryLoader.load(true); + }); + 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 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: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; +} + +/** + 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() { + return supercompressionScheme == SupercompressionScheme.ZStandard; + } +} + +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; +} + +typedef KTX2Sample = { + 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 { + /** + 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: 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}'; + } + } + + /** + Check if texture data is in gamma space. + + @return True if texture is in gamma space + **/ + public function isInGammaSpace() { + return transferFunction == DFDTransferFunction.SRGB; + } +} + +typedef KTX2ImageDesc = { + 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; +} + +@: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; + cTFBC1; + cTFBC3; + cTFBC4; + cTFBC5; + cTFBC7_M6_OPAQUE_ONLY; + cTFBC7_M5; + cTFPVRTC1_4_RGB; + cTFPVRTC1_4_RGBA; + cTFASTC_4x4; + cTFATC_RGB1; + cTFATC_RGBA_INTERPOLATED_ALPHA2; + cTFRGBA321; + cTFRGB5654; + cTFBGR5655; + cTFRGBA44446; +} + +@: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; +} + +class InternalFormat { + public static final COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; +} + +/** + * 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; +} + +typedef WorkerTask = { + 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, +} + +@:structInit class BasisWorkerMessage { + public final id:String; + public final type = 'transcode'; + 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, +}