From ddcb1f4eff4f0a9d80141d183ef700c3ada84e98 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 13 Nov 2025 10:08:53 +0100 Subject: [PATCH 1/9] compressed texture update --- examples/tests/tx-compression.ts | 23 ++++- src/core/CoreTextureManager.ts | 15 +++ src/core/lib/textureCompression.ts | 91 +++++++++++++++++-- .../renderers/webgl/WebGlCoreCtxTexture.ts | 23 ++++- src/core/renderers/webgl/WebGlCoreRenderer.ts | 24 ++--- src/core/textures/ImageTexture.ts | 8 +- src/core/textures/Texture.ts | 11 ++- 7 files changed, 164 insertions(+), 31 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index 8031c850..79622fde 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -31,15 +31,25 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - renderer.createNode({ + const url1 = + 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=200&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + const url2 = + 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=199&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + + const ktx1 = renderer.createNode({ x: 100, y: 170, width: 550, height: 550, - src: '../assets/test-etc1.pvr', + src: url1, + imageType: 'ktx', parent: testRoot, }); + ktx1.on('loaded', (_, data) => { + console.log('pvr loaded', data.dimensions); + }); + renderer.createTextNode({ x: 800, y: 100, @@ -51,12 +61,17 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - renderer.createNode({ + const ktx2 = renderer.createNode({ x: 800, y: 170, width: 400, height: 400, - src: '../assets/test-s3tc.ktx', + src: url2, + imageType: 'ktx', parent: testRoot, }); + + ktx2.on('loaded', (_, data) => { + console.log('ktx loaded', data.dimensions); + }); } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 7bdc23e9..b5ed2578 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -176,6 +176,21 @@ export interface TextureOptions { * The resize modes cover and contain are supported */ resizeMode?: ResizeModeOptions; + + /** + * Width info used for compressed textures. Otherwise falls back to node width. + */ + width?: number; + + /** + * Height info used for compressed textures. Otherwise falls back to node height. + */ + height?: number; + + /** + * Chunk size VxV, default value is 1. Used for Compressed textures + */ + chunkSize?: number; } export class CoreTextureManager extends EventEmitter { diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index 7fa2327e..9deaf286 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import type { ImageTextureProps } from '../textures/ImageTexture.js'; import { type TextureData } from '../textures/Texture.js'; /** @@ -27,8 +28,13 @@ import { type TextureData } from '../textures/Texture.js'; * and only supports the following extensions: .ktx and .pvr * @returns */ -export function isCompressedTextureContainer(url: string): boolean { - return /\.(ktx|pvr)$/.test(url); +export function isCompressedTextureContainer( + props: ImageTextureProps, +): boolean { + if (props.type === 'ktx' || props.type === 'pvr') { + return true; + } + return /\.(ktx|pvr)$/.test(props.src as string); } /** @@ -38,10 +44,10 @@ export function isCompressedTextureContainer(url: string): boolean { */ export const loadCompressedTexture = async ( url: string, + props: ImageTextureProps, ): Promise => { try { const response = await fetch(url); - if (!response.ok) { throw new Error( `Failed to fetch compressed texture: ${response.status} ${response.statusText}`, @@ -49,8 +55,7 @@ export const loadCompressedTexture = async ( } const arrayBuffer = await response.arrayBuffer(); - - if (url.indexOf('.ktx') !== -1) { + if (props.type === 'ktx' || url.indexOf('.ktx') !== -1) { return loadKTXData(arrayBuffer); } @@ -94,6 +99,7 @@ const loadKTXData = async (buffer: ArrayBuffer): Promise => { return { data: { + blockInfo: getCompressedBlockInfo(data.glInternalFormat), glInternalFormat: data.glInternalFormat, mipmaps, width: data.pixelWidth || 0, @@ -149,11 +155,11 @@ const loadPVRData = async (buffer: ArrayBuffer): Promise => { width = width >> 1; height = height >> 1; } - return { data: { + blockInfo: getCompressedBlockInfo(pvrFormatEtc1), glInternalFormat: pvrFormatEtc1, - mipmaps: mipmaps, + mipmaps: mipmaps as unknown as ArrayBuffer[], width: data.pixelWidth || 0, height: data.pixelHeight || 0, type: 'pvr', @@ -161,3 +167,74 @@ const loadPVRData = async (buffer: ArrayBuffer): Promise => { premultiplyAlpha: false, }; }; + +/** + * Get compressed texture block info for a numeric WebGL internalFormat value. + * Returns { width, height, bytes } or null if unknown. + */ +function getCompressedBlockInfo(internalFormat: number) { + switch (internalFormat) { + // --- S3TC / DXTn (WEBGL_compressed_texture_s3tc, sRGB variants) --- + case 0x83f0: // COMPRESSED_RGB_S3TC_DXT1_EXT + case 0x83f1: // COMPRESSED_RGBA_S3TC_DXT1_EXT + case 0x8c4c: // COMPRESSED_SRGB_S3TC_DXT1_EXT + case 0x8c4d: // COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT + return { width: 4, height: 4, bytes: 8 }; + + case 0x83f2: // COMPRESSED_RGBA_S3TC_DXT3_EXT + case 0x83f3: // COMPRESSED_RGBA_S3TC_DXT5_EXT + case 0x8c4e: // COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT + case 0x8c4f: // COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT + return { width: 4, height: 4, bytes: 16 }; + + // --- ETC1 / ETC2 / EAC --- + case 0x8d64: // COMPRESSED_RGB_ETC1_WEBGL + case 0x9274: // COMPRESSED_RGB8_ETC2 + case 0x9275: // COMPRESSED_SRGB8_ETC2 + case 0x9270: // COMPRESSED_R11_EAC + return { width: 4, height: 4, bytes: 8 }; + + case 0x9278: // COMPRESSED_RGBA8_ETC2_EAC + case 0x9279: // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC + case 0x9272: // COMPRESSED_RG11_EAC + return { width: 4, height: 4, bytes: 16 }; + + // --- PVRTC (WEBGL_compressed_texture_pvrtc) --- + case 0x8c00: // COMPRESSED_RGB_PVRTC_4BPPV1_IMG + case 0x8c02: // COMPRESSED_RGBA_PVRTC_4BPPV1_IMG + return { width: 4, height: 4, bytes: 8 }; + + case 0x8c01: // COMPRESSED_RGB_PVRTC_2BPPV1_IMG + case 0x8c03: // COMPRESSED_RGBA_PVRTC_2BPPV1_IMG + return { width: 8, height: 4, bytes: 8 }; + + // --- ASTC (WEBGL_compressed_texture_astc) --- + case 0x93b0: // COMPRESSED_RGBA_ASTC_4x4_KHR + case 0x93d0: // COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR + return { width: 4, height: 4, bytes: 16 }; + + case 0x93b1: // 5x5 + case 0x93d1: + return { width: 5, height: 5, bytes: 16 }; + + case 0x93b2: // 6x6 + case 0x93d2: + return { width: 6, height: 6, bytes: 16 }; + + case 0x93b3: // 8x8 + case 0x93d3: + return { width: 8, height: 8, bytes: 16 }; + + case 0x93b4: // 10x10 + case 0x93d4: + return { width: 10, height: 10, bytes: 16 }; + + case 0x93b5: // 12x12 + case 0x93d5: + return { width: 12, height: 12, bytes: 16 }; + + default: + console.warn('Unknown or unsupported compressed format:', internalFormat); + return { width: 1, height: 1, bytes: 4 }; + } +} diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index b9da6038..391a02b5 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -43,6 +43,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { private _w = 0; private _h = 0; + txCoordX1 = 0; + txCoordY1 = 0; + txCoordX2 = 1; + txCoordY2 = 1; + constructor( protected glw: WebGlContextWrapper, memManager: TextureMemoryManager, @@ -211,15 +216,24 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { this.setTextureMemUse(height * width * formatBytes * memoryPadding); } else if (tdata && 'mipmaps' in tdata && tdata.mipmaps) { - const { mipmaps, width = 0, height = 0, type, glInternalFormat } = tdata; + const { + mipmaps, + width: w = 0, + height: h = 0, + type, + glInternalFormat, + blockInfo, + } = tdata; + console.log('derp', tdata); const view = type === 'ktx' ? new DataView(mipmaps[0] ?? new ArrayBuffer(0)) : (mipmaps[0] as unknown as ArrayBufferView); + console.log('view', view); glw.bindTexture(this._nativeCtxTexture); - glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view); + glw.compressedTexImage2D(0, glInternalFormat, w, h, 0, view); glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); @@ -230,6 +244,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return { width: 0, height: 0 }; } + this.txCoordX2 = w / (Math.ceil(w / blockInfo.width) * blockInfo.width); + this.txCoordY2 = h / (Math.ceil(h / blockInfo.height) * blockInfo.height); + + width = w; + height = h; this.setTextureMemUse(view.byteLength); } else if (tdata && tdata instanceof Uint8Array) { // Color Texture diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index fcc0b38a..9dc41a36 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -269,10 +269,18 @@ export class WebGlCoreRenderer extends CoreRenderer { assertTruthy(curRenderOp); } - let texCoordX1 = 0; - let texCoordY1 = 0; - let texCoordX2 = 1; - let texCoordY2 = 1; + let ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; + if (ctxTexture === undefined) { + ctxTexture = this.stage.defaultTexture?.ctxTexture as WebGlCoreCtxTexture; + console.warn( + 'WebGL Renderer: Texture does not have a ctxTexture, using default texture instead', + ); + } + + let texCoordX1 = ctxTexture.txCoordX1; + let texCoordY1 = ctxTexture.txCoordY1; + let texCoordX2 = ctxTexture.txCoordX2; + let texCoordY2 = ctxTexture.txCoordY2; if (texture.type === TextureType.subTexture) { const { @@ -335,14 +343,6 @@ export class WebGlCoreRenderer extends CoreRenderer { [texCoordY1, texCoordY2] = [texCoordY2, texCoordY1]; } - let ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - if (ctxTexture === undefined) { - ctxTexture = this.stage.defaultTexture?.ctxTexture as WebGlCoreCtxTexture; - console.warn( - 'WebGL Renderer: Texture does not have a ctxTexture, using default texture instead', - ); - } - const textureIdx = this.addTexture(ctxTexture, bufferIdx); assertTruthy(this.curRenderOp !== null); diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 35d2ee58..e0af237a 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -76,7 +76,7 @@ export interface ImageTextureProps { * * @default null */ - type?: 'regular' | 'compressed' | 'svg' | null; + type?: 'regular' | 'compressed' | 'pvr' | 'ktx' | 'svg' | null; /** * The width of the rectangle from which the ImageBitmap will be extracted. This value * can be negative. Only works when createImageBitmap is supported on the browser. @@ -355,11 +355,11 @@ export class ImageTexture extends Texture { } if (type === 'compressed') { - return loadCompressedTexture(absoluteSrc); + return loadCompressedTexture(absoluteSrc, this.props); } - if (isCompressedTextureContainer(src) === true) { - return loadCompressedTexture(absoluteSrc); + if (isCompressedTextureContainer(this.props) === true) { + return loadCompressedTexture(absoluteSrc, this.props); } // default diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index af4ec87c..ec5795f1 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -72,6 +72,15 @@ interface CompressedData { * The height of the compressed texture in pixels. **/ height: number; + + /** + * block info + */ + blockInfo: { + width: number; + height: number; + bytes: number; + }; } /** @@ -298,7 +307,6 @@ export abstract class Texture extends EventEmitter { // We've exceeded the max retry count, do not attempt to load again return; } - this.txManager.loadTexture(this); } @@ -386,7 +394,6 @@ export abstract class Texture extends EventEmitter { if (this.state === state) { return; } - let payload: Error | Dimensions | null = null; if (state === 'loaded') { // Clear any previous error when successfully loading From d36737dd898b9c76cdb84a6e52d6bcf70dbab612 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 13 Nov 2025 11:13:09 +0100 Subject: [PATCH 2/9] fixed compression scaling --- examples/tests/tx-compression.ts | 22 +++--- src/core/lib/textureCompression.ts | 16 +++-- .../renderers/webgl/WebGlCoreCtxTexture.ts | 71 ++++++++++++++----- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index 79622fde..2cf49d70 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -25,16 +25,16 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { y: 100, color: 0xffffffff, alpha: 1.0, - text: 'etc1 compression in .pvr', + text: 'w: 400', fontFamily: 'Ubuntu', fontSize: 30, parent: testRoot, }); const url1 = - 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=200&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=400&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; const url2 = - 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=199&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=333&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; const ktx1 = renderer.createNode({ x: 100, @@ -46,8 +46,11 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - ktx1.on('loaded', (_, data) => { - console.log('pvr loaded', data.dimensions); + ktx1.on('loaded', (node, data) => { + console.log('ktx1 loaded'); + const { width, height } = data.dimensions; + node.width = width; + node.height = height; }); renderer.createTextNode({ @@ -55,7 +58,7 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { y: 100, color: 0xffffffff, alpha: 1.0, - text: 's3tc compression in .ktx', + text: 'w: 333', fontFamily: 'Ubuntu', fontSize: 30, parent: testRoot, @@ -71,7 +74,10 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - ktx2.on('loaded', (_, data) => { - console.log('ktx loaded', data.dimensions); + ktx2.on('loaded', (node, data) => { + console.log('ktx2 loaded'); + const { width, height } = data.dimensions; + node.width = width; + node.height = height; }); } diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index 9deaf286..d0bd30b0 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -16,7 +16,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import type { ImageTextureProps } from '../textures/ImageTexture.js'; import { type TextureData } from '../textures/Texture.js'; @@ -83,18 +82,21 @@ const loadKTXData = async (buffer: ArrayBuffer): Promise => { bytesOfKeyValueData: view.getUint32(60, littleEndian), }; - let offset = 64; - // Key Value Pairs of data start at byte offset 64 // But the only known kvp is the API version, so skipping parsing. - offset += data.bytesOfKeyValueData; + let offset = 64 + data.bytesOfKeyValueData; for (let i = 0; i < data.numberOfMipmapLevels; i++) { - const imageSize = view.getUint32(offset); + const imageSize = view.getUint32(offset, littleEndian); offset += 4; - mipmaps.push(view.buffer.slice(offset, imageSize)); - offset += imageSize; + const end = offset + imageSize; + + mipmaps.push(view.buffer.slice(offset, end)); + offset = end; + if (offset % 4 !== 0) { + offset += 4 - (offset % 4); + } } return { diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 391a02b5..e1980a5f 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -216,24 +216,57 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { this.setTextureMemUse(height * width * formatBytes * memoryPadding); } else if (tdata && 'mipmaps' in tdata && tdata.mipmaps) { - const { - mipmaps, - width: w = 0, - height: h = 0, - type, - glInternalFormat, - blockInfo, - } = tdata; - console.log('derp', tdata); - const view = - type === 'ktx' - ? new DataView(mipmaps[0] ?? new ArrayBuffer(0)) - : (mipmaps[0] as unknown as ArrayBufferView); - - console.log('view', view); + const { mipmaps, type, glInternalFormat, blockInfo } = tdata; + + let w = tdata.width; + let h = tdata.height; + glw.bindTexture(this._nativeCtxTexture); - glw.compressedTexImage2D(0, glInternalFormat, w, h, 0, view); + const blockWidth = blockInfo.width; + const blockHeight = blockInfo.height; + + for (let i = 0; i < mipmaps.length; i++) { + let view = + type === 'ktx' + ? new Uint8Array(mipmaps[i] ?? new ArrayBuffer(0)) + : new Uint8Array( + (mipmaps[i]! as ArrayBufferView).buffer, + (mipmaps[i]! as ArrayBufferView).byteOffset, + (mipmaps[i]! as ArrayBufferView).byteLength, + ); + + const uploadW = Math.ceil(w / blockWidth) * blockWidth; + const uploadH = Math.ceil(h / blockHeight) * blockHeight; + + const expectedBytes = + Math.ceil(w / blockWidth) * + Math.ceil(h / blockHeight) * + blockInfo.bytes; + + if (view.byteLength < expectedBytes) { + const padded = new Uint8Array(expectedBytes); + padded.set(view); + view = padded; + } + console.log( + `Level ${i}: ${w}x${h}, (upload ${uploadW}x${uploadH}), data=${ + view.byteLength + }, format=0x${glInternalFormat.toString(16)}`, + ); + glw.compressedTexImage2D( + i, + glInternalFormat, + uploadW, + uploadH, + 0, + view, + ); + + w = Math.max(1, w >> 1); + h = Math.max(1, h >> 1); + } + glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); @@ -247,9 +280,9 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { this.txCoordX2 = w / (Math.ceil(w / blockInfo.width) * blockInfo.width); this.txCoordY2 = h / (Math.ceil(h / blockInfo.height) * blockInfo.height); - width = w; - height = h; - this.setTextureMemUse(view.byteLength); + width = tdata.width; + height = tdata.height; + this.setTextureMemUse(mipmaps[0]?.byteLength ?? 0); } else if (tdata && tdata instanceof Uint8Array) { // Color Texture width = 1; From 7be7169ee583f7ded71c8d666bff7bb776030f25 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 13 Nov 2025 11:24:58 +0100 Subject: [PATCH 3/9] fix texcoord scaling --- .../renderers/webgl/WebGlCoreCtxTexture.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index e1980a5f..d0ea3b87 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -231,9 +231,9 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { type === 'ktx' ? new Uint8Array(mipmaps[i] ?? new ArrayBuffer(0)) : new Uint8Array( - (mipmaps[i]! as ArrayBufferView).buffer, - (mipmaps[i]! as ArrayBufferView).byteOffset, - (mipmaps[i]! as ArrayBufferView).byteLength, + (mipmaps[i] as unknown as ArrayBufferView).buffer, + (mipmaps[i] as unknown as ArrayBufferView).byteOffset, + (mipmaps[i] as unknown as ArrayBufferView).byteLength, ); const uploadW = Math.ceil(w / blockWidth) * blockWidth; @@ -249,11 +249,6 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { padded.set(view); view = padded; } - console.log( - `Level ${i}: ${w}x${h}, (upload ${uploadW}x${uploadH}), data=${ - view.byteLength - }, format=0x${glInternalFormat.toString(16)}`, - ); glw.compressedTexImage2D( i, glInternalFormat, @@ -277,11 +272,13 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return { width: 0, height: 0 }; } - this.txCoordX2 = w / (Math.ceil(w / blockInfo.width) * blockInfo.width); - this.txCoordY2 = h / (Math.ceil(h / blockInfo.height) * blockInfo.height); - width = tdata.width; height = tdata.height; + this.txCoordX2 = + width / (Math.ceil(width / blockInfo.width) * blockInfo.width); + this.txCoordY2 = + height / (Math.ceil(height / blockInfo.height) * blockInfo.height); + this.setTextureMemUse(mipmaps[0]?.byteLength ?? 0); } else if (tdata && tdata instanceof Uint8Array) { // Color Texture From b6cb63be221c0afbe3e4d78ddb7042635a145330 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 13 Nov 2025 11:28:22 +0100 Subject: [PATCH 4/9] tweaked test a bit --- examples/tests/tx-compression.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index 2cf49d70..db83bbf2 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -20,7 +20,7 @@ import type { ExampleSettings } from '../common/ExampleSettings.js'; export default async function ({ renderer, testRoot }: ExampleSettings) { - renderer.createTextNode({ + const ktx1Label = renderer.createTextNode({ x: 100, y: 100, color: 0xffffffff, @@ -51,9 +51,11 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { const { width, height } = data.dimensions; node.width = width; node.height = height; + + ktx1Label.text = `w: ${width}, h: ${height}`; }); - renderer.createTextNode({ + const ktx2Label = renderer.createTextNode({ x: 800, y: 100, color: 0xffffffff, @@ -79,5 +81,6 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { const { width, height } = data.dimensions; node.width = width; node.height = height; + ktx2Label.text = `w: ${width}, h: ${height}`; }); } From e5a1901d29ef74d0923fef1089fff19d51f126ab Mon Sep 17 00:00:00 2001 From: jfboeve Date: Fri, 14 Nov 2025 16:03:17 +0100 Subject: [PATCH 5/9] cleaned up textureCompression code --- examples/tests/tx-compression.ts | 8 +- src/core/lib/WebGlContextWrapper.ts | 2 + src/core/lib/textureCompression.ts | 536 +++++++++++++----- .../renderers/webgl/WebGlCoreCtxTexture.ts | 53 +- src/core/textures/ImageTexture.ts | 4 +- src/core/textures/Texture.ts | 8 +- 6 files changed, 412 insertions(+), 199 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index db83bbf2..d4e00704 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -33,16 +33,18 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { const url1 = 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=400&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; - const url2 = - 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=333&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + + const randomWidth = Math.floor(Math.random() * 300) + 200; + const url2 = `https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=${randomWidth}&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast`; const ktx1 = renderer.createNode({ x: 100, y: 170, width: 550, height: 550, + // src: '../assets/test-etc1.pvr', src: url1, - imageType: 'ktx', + imageType: 'compressed', parent: testRoot, }); diff --git a/src/core/lib/WebGlContextWrapper.ts b/src/core/lib/WebGlContextWrapper.ts index 7953ca19..21eacd83 100644 --- a/src/core/lib/WebGlContextWrapper.ts +++ b/src/core/lib/WebGlContextWrapper.ts @@ -67,6 +67,7 @@ export class WebGlContextWrapper { public readonly TEXTURE_WRAP_S; public readonly TEXTURE_WRAP_T; public readonly LINEAR; + public readonly LINEAR_MIPMAP_LINEAR; public readonly CLAMP_TO_EDGE; public readonly RGB; public readonly RGBA; @@ -158,6 +159,7 @@ export class WebGlContextWrapper { this.TEXTURE_WRAP_S = gl.TEXTURE_WRAP_S; this.TEXTURE_WRAP_T = gl.TEXTURE_WRAP_T; this.LINEAR = gl.LINEAR; + this.LINEAR_MIPMAP_LINEAR = gl.LINEAR_MIPMAP_LINEAR; this.CLAMP_TO_EDGE = gl.CLAMP_TO_EDGE; this.RGB = gl.RGB; this.RGBA = gl.RGBA; diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index d0bd30b0..f7bd6562 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -17,7 +17,14 @@ * limitations under the License. */ import type { ImageTextureProps } from '../textures/ImageTexture.js'; -import { type TextureData } from '../textures/Texture.js'; +import { type CompressedData, type TextureData } from '../textures/Texture.js'; +import type { WebGlContextWrapper } from './WebGlContextWrapper.js'; + +export type UploadCompressedTextureFunction = ( + glw: WebGlContextWrapper, + texture: WebGLTexture, + data: CompressedData, +) => void; /** * Tests if the given location is a compressed texture container @@ -36,6 +43,34 @@ export function isCompressedTextureContainer( return /\.(ktx|pvr)$/.test(props.src as string); } +const PVR_MAGIC = 0x03525650; // 'PVR3' in little-endian +const PVR_TO_GL_INTERNAL_FORMAT: Record = { + 0: 0x8c01, + 1: 0x8c03, + 2: 0x8c00, + 3: 0x8c02, // PVRTC1 + 6: 0x8d64, // ETC1 + 7: 0x83f0, + 8: 0x83f2, + 9: 0x83f2, + 10: 0x83f3, + 11: 0x83f3, // DXT variants +}; +const ASTC_MAGIC = 0x5ca1ab13; + +const ASTC_TO_GL_INTERNAL_FORMAT: Record = { + '4x4': 0x93b0, // COMPRESSED_RGBA_ASTC_4x4_KHR + '5x5': 0x93b1, // COMPRESSED_RGBA_ASTC_5x5_KHR + '6x6': 0x93b2, // COMPRESSED_RGBA_ASTC_6x6_KHR + '8x8': 0x93b3, // COMPRESSED_RGBA_ASTC_8x8_KHR + '10x10': 0x93b4, // COMPRESSED_RGBA_ASTC_10x10_KHR + '12x12': 0x93b5, // COMPRESSED_RGBA_ASTC_12x12_KHR +}; + +// KTX file identifier +const KTX_IDENTIFIER = [ + 0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a, +]; /** * Loads a compressed texture container * @param url @@ -43,7 +78,6 @@ export function isCompressedTextureContainer( */ export const loadCompressedTexture = async ( url: string, - props: ImageTextureProps, ): Promise => { try { const response = await fetch(url); @@ -54,39 +88,156 @@ export const loadCompressedTexture = async ( } const arrayBuffer = await response.arrayBuffer(); - if (props.type === 'ktx' || url.indexOf('.ktx') !== -1) { - return loadKTXData(arrayBuffer); + const view = new DataView(arrayBuffer); + const magic = view.getUint32(0, true); + console.log('magic', magic); + if (magic === PVR_MAGIC) { + console.log('pvr'); + return loadPVR(view); + } + + if (magic === ASTC_MAGIC) { + return loadASTC(view); } - return loadPVRData(arrayBuffer); + for (let i = 0; i < KTX_IDENTIFIER.length; i++) { + if (view.getUint8(i) !== KTX_IDENTIFIER[i]) { + throw new Error('Unrecognized compressed texture format'); + } + } + return loadKTX(view); } catch (error) { throw new Error(`Failed to load compressed texture from ${url}: ${error}`); } }; +function readUint24(view: DataView, offset: number) { + return ( + view.getUint8(offset) + + (view.getUint8(offset + 1) << 8) + + (view.getUint8(offset + 2) << 16) + ); +} + /** - * Loads a KTX texture container and returns the texture data - * @param buffer + * Loads an ASTC texture container and returns the texture data + * @param view * @returns */ -const loadKTXData = async (buffer: ArrayBuffer): Promise => { - const view = new DataView(buffer); - const littleEndian = view.getUint32(12) === 16909060 ? true : false; - const mipmaps = []; - - const data = { - glInternalFormat: view.getUint32(28, littleEndian), - pixelWidth: view.getUint32(36, littleEndian), - pixelHeight: view.getUint32(40, littleEndian), - numberOfMipmapLevels: view.getUint32(56, littleEndian), - bytesOfKeyValueData: view.getUint32(60, littleEndian), +const loadASTC = async function ( + view: DataView, +): Promise { + const blockX = view.getUint8(4); + const blockY = view.getUint8(5); + const sizeX = readUint24(view, 7); + const sizeY = readUint24(view, 10); + + if (sizeX === 0 || sizeY === 0) { + throw new Error(`Invalid ASTC texture dimensions: ${sizeX}x${sizeY}`); + } + const expected = Math.ceil(sizeX / blockX) * Math.ceil(sizeY / blockY) * 16; + const dataSize = view.byteLength - 16; + if (expected !== dataSize) { + throw new Error( + `Invalid ASTC texture data size: expected ${expected}, got ${dataSize}`, + ); + } + + const internalFormat = ASTC_TO_GL_INTERNAL_FORMAT[`${blockX}x${blockY}`]; + if (internalFormat === undefined) { + throw new Error(`Unsupported ASTC block size: ${blockX}x${blockY}`); + } + + const mipmaps: ArrayBuffer[] = []; + mipmaps.push(view.buffer.slice(16)); + + return { + data: { + blockInfo: blockInfoMap[internalFormat]!, + glInternalFormat: internalFormat, + mipmaps, + width: sizeX, + height: sizeY, + type: 'astc', + }, + premultiplyAlpha: false, }; +}; + +const uploadASTC = function ( + glw: WebGlContextWrapper, + texture: WebGLTexture, + data: CompressedData, +) { + if (glw.getExtension('WEBGL_compressed_texture_astc') === null) { + throw new Error('ASTC compressed textures not supported by this device'); + } + + glw.bindTexture(texture); + + const { glInternalFormat, mipmaps, width, height } = data; + + const view = new Uint8Array(mipmaps[0]!); + + glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view); + + // ASTC textures MUST use no mipmaps unless stored + glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); + glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); +}; +/** + * Loads a KTX texture container and returns the texture data + * @param view + * @returns + */ +const loadKTX = async function ( + view: DataView, +): Promise { + const endianness = view.getUint32(12, true); + const littleEndian = endianness === 0x04030201; + if (littleEndian === false && endianness !== 0x01020304) { + throw new Error('Invalid KTX endianness value'); + } - // Key Value Pairs of data start at byte offset 64 - // But the only known kvp is the API version, so skipping parsing. - let offset = 64 + data.bytesOfKeyValueData; + const glType = view.getUint32(16, littleEndian); + const glFormat = view.getUint32(24, littleEndian); + if (glType !== 0 || glFormat !== 0) { + throw new Error( + `KTX texture is not compressed (glType: ${glType}, glFormat: ${glFormat})`, + ); + } - for (let i = 0; i < data.numberOfMipmapLevels; i++) { + const glInternalFormat = view.getUint32(28, littleEndian); + if (blockInfoMap[glInternalFormat] === undefined) { + throw new Error( + `Unsupported KTX compressed texture format: 0x${glInternalFormat.toString( + 16, + )}`, + ); + } + + const width = view.getUint32(36, littleEndian); + const height = view.getUint32(40, littleEndian); + if (width === 0 || height === 0) { + throw new Error(`Invalid KTX texture dimensions: ${width}x${height}`); + } + + const mipmapLevels = view.getUint32(56, littleEndian); + if (mipmapLevels === 0) { + throw new Error('KTX texture has no mipmap levels'); + } + + const bytesOfKeyValueData = view.getUint32(60, littleEndian); + const mipmaps: ArrayBuffer[] = []; + let offset = 64 + bytesOfKeyValueData; + + if (offset > view.byteLength) { + throw new Error('Invalid KTX file: key/value data exceeds file size'); + } + + for (let i = 0; i < mipmapLevels; i++) { const imageSize = view.getUint32(offset, littleEndian); offset += 4; @@ -101,142 +252,247 @@ const loadKTXData = async (buffer: ArrayBuffer): Promise => { return { data: { - blockInfo: getCompressedBlockInfo(data.glInternalFormat), - glInternalFormat: data.glInternalFormat, + blockInfo: blockInfoMap[glInternalFormat]!, + glInternalFormat: glInternalFormat, mipmaps, - width: data.pixelWidth || 0, - height: data.pixelHeight || 0, + width: width, + height: height, type: 'ktx', }, premultiplyAlpha: false, }; }; -/** - * Loads a PVR texture container and returns the texture data - * @param buffer - * @returns - */ -const loadPVRData = async (buffer: ArrayBuffer): Promise => { - // pvr header length in 32 bits - const pvrHeaderLength = 13; - // for now only we only support: COMPRESSED_RGB_ETC1_WEBGL - const pvrFormatEtc1 = 0x8d64; - const pvrWidth = 7; - const pvrHeight = 6; - const pvrMipmapCount = 11; - const pvrMetadata = 12; - const arrayBuffer = buffer; - const header = new Int32Array(arrayBuffer, 0, pvrHeaderLength); - - // @ts-expect-error Object possibly undefined - - const dataOffset = header[pvrMetadata] + 52; - const pvrtcData = new Uint8Array(arrayBuffer, dataOffset); - const mipmaps = []; - const data = { - pixelWidth: header[pvrWidth], - pixelHeight: header[pvrHeight], - numberOfMipmapLevels: header[pvrMipmapCount] || 0, - }; +const uploadKTX = function ( + glw: WebGlContextWrapper, + texture: WebGLTexture, + data: CompressedData, +) { + const { glInternalFormat, mipmaps, width, height, blockInfo } = data; + + glw.bindTexture(texture); + + const blockWidth = blockInfo.width; + const blockHeight = blockInfo.height; + let w = width; + let h = height; + + for (let i = 0; i < mipmaps.length; i++) { + let view = new Uint8Array(mipmaps[i]!); + + const uploadW = Math.ceil(w / blockWidth) * blockWidth; + const uploadH = Math.ceil(h / blockHeight) * blockHeight; + + const expectedBytes = + Math.ceil(w / blockWidth) * Math.ceil(h / blockHeight) * blockInfo.bytes; + + if (view.byteLength < expectedBytes) { + const padded = new Uint8Array(expectedBytes); + padded.set(view); + view = padded; + } + + glw.compressedTexImage2D(i, glInternalFormat, uploadW, uploadH, 0, view); + + w = Math.max(1, w >> 1); + h = Math.max(1, h >> 1); + } - let offset = 0; - let width = data.pixelWidth || 0; - let height = data.pixelHeight || 0; + glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); + glw.texParameteri( + glw.TEXTURE_MIN_FILTER, + mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR, + ); +}; + +function pvrtcMipSize(width: number, height: number, bpp: 2 | 4) { + const minW = bpp === 2 ? 16 : 8; + const minH = 8; + const w = Math.max(width, minW); + const h = Math.max(height, minH); + return (w * h * bpp) / 8; +} - for (let i = 0; i < data.numberOfMipmapLevels; i++) { - const level = ((width + 3) >> 2) * ((height + 3) >> 2) * 8; - const view = new Uint8Array( - arrayBuffer, - pvrtcData.byteOffset + offset, - level, +const loadPVR = async function ( + view: DataView, +): Promise { + const pixelFormatLow = view.getUint32(8, true); + const internalFormat = PVR_TO_GL_INTERNAL_FORMAT[pixelFormatLow]; + + if (internalFormat === undefined) { + throw new Error( + `Unsupported PVR pixel format: 0x${pixelFormatLow.toString(16)}`, ); + } + + const height = view.getInt32(24, true); + const width = view.getInt32(28, true); + + // validate dimensions + if (width === 0 || height === 0) { + throw new Error(`Invalid PVR texture dimensions: ${width}x${height}`); + } + const mipmapLevels = view.getInt32(44, true); + const metadataSize = view.getUint32(48, true); + const buffer = view.buffer; + + let offset = 52 + metadataSize; + if (offset > buffer.byteLength) { + throw new Error('Invalid PVR file: metadata exceeds file size'); + } + + const mipmaps: ArrayBuffer[] = []; + + const block = blockInfoMap[internalFormat]!; + + for (let i = 0; i < mipmapLevels; i++) { + const declaredSize = view.getUint32(offset, true); + const max = buffer.byteLength - (offset + 4); + + if (declaredSize > 0 && declaredSize <= max) { + offset += 4; + const start = offset; + const end = offset + declaredSize; + + mipmaps.push(buffer.slice(start, end)); + offset = end; + offset = (offset + 3) & ~3; // align to 4 bytes + continue; + } - mipmaps.push(view); - offset += level; - width = width >> 1; - height = height >> 1; + if ( + pixelFormatLow === 0 || + pixelFormatLow === 1 || + pixelFormatLow === 2 || + pixelFormatLow === 3 + ) { + const bpp = pixelFormatLow === 0 || pixelFormatLow === 1 ? 2 : 4; + const computed = pvrtcMipSize(width >> i, height >> i, bpp); + + mipmaps.push(buffer.slice(offset, offset + computed)); + offset += computed; + offset = (offset + 3) & ~3; // align to 4 bytes + continue; + } + + if (block !== undefined) { + const blockW = Math.ceil((width >> i) / block.width); + const blockH = Math.ceil((height >> i) / block.height); + const computed = blockW * blockH * block.bytes; + + mipmaps.push(buffer.slice(offset, offset + computed)); + offset += computed; + offset = (offset + 3) & ~3; + } } + return { data: { - blockInfo: getCompressedBlockInfo(pvrFormatEtc1), - glInternalFormat: pvrFormatEtc1, - mipmaps: mipmaps as unknown as ArrayBuffer[], - width: data.pixelWidth || 0, - height: data.pixelHeight || 0, + blockInfo: blockInfoMap[internalFormat]!, + glInternalFormat: internalFormat, + mipmaps, + width: width, + height: height, type: 'pvr', }, premultiplyAlpha: false, }; }; -/** - * Get compressed texture block info for a numeric WebGL internalFormat value. - * Returns { width, height, bytes } or null if unknown. - */ -function getCompressedBlockInfo(internalFormat: number) { - switch (internalFormat) { - // --- S3TC / DXTn (WEBGL_compressed_texture_s3tc, sRGB variants) --- - case 0x83f0: // COMPRESSED_RGB_S3TC_DXT1_EXT - case 0x83f1: // COMPRESSED_RGBA_S3TC_DXT1_EXT - case 0x8c4c: // COMPRESSED_SRGB_S3TC_DXT1_EXT - case 0x8c4d: // COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT - return { width: 4, height: 4, bytes: 8 }; - - case 0x83f2: // COMPRESSED_RGBA_S3TC_DXT3_EXT - case 0x83f3: // COMPRESSED_RGBA_S3TC_DXT5_EXT - case 0x8c4e: // COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT - case 0x8c4f: // COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT - return { width: 4, height: 4, bytes: 16 }; - - // --- ETC1 / ETC2 / EAC --- - case 0x8d64: // COMPRESSED_RGB_ETC1_WEBGL - case 0x9274: // COMPRESSED_RGB8_ETC2 - case 0x9275: // COMPRESSED_SRGB8_ETC2 - case 0x9270: // COMPRESSED_R11_EAC - return { width: 4, height: 4, bytes: 8 }; - - case 0x9278: // COMPRESSED_RGBA8_ETC2_EAC - case 0x9279: // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC - case 0x9272: // COMPRESSED_RG11_EAC - return { width: 4, height: 4, bytes: 16 }; - - // --- PVRTC (WEBGL_compressed_texture_pvrtc) --- - case 0x8c00: // COMPRESSED_RGB_PVRTC_4BPPV1_IMG - case 0x8c02: // COMPRESSED_RGBA_PVRTC_4BPPV1_IMG - return { width: 4, height: 4, bytes: 8 }; - - case 0x8c01: // COMPRESSED_RGB_PVRTC_2BPPV1_IMG - case 0x8c03: // COMPRESSED_RGBA_PVRTC_2BPPV1_IMG - return { width: 8, height: 4, bytes: 8 }; - - // --- ASTC (WEBGL_compressed_texture_astc) --- - case 0x93b0: // COMPRESSED_RGBA_ASTC_4x4_KHR - case 0x93d0: // COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR - return { width: 4, height: 4, bytes: 16 }; - - case 0x93b1: // 5x5 - case 0x93d1: - return { width: 5, height: 5, bytes: 16 }; - - case 0x93b2: // 6x6 - case 0x93d2: - return { width: 6, height: 6, bytes: 16 }; - - case 0x93b3: // 8x8 - case 0x93d3: - return { width: 8, height: 8, bytes: 16 }; - - case 0x93b4: // 10x10 - case 0x93d4: - return { width: 10, height: 10, bytes: 16 }; - - case 0x93b5: // 12x12 - case 0x93d5: - return { width: 12, height: 12, bytes: 16 }; - - default: - console.warn('Unknown or unsupported compressed format:', internalFormat); - return { width: 1, height: 1, bytes: 4 }; +const uploadPVR = function ( + glw: WebGlContextWrapper, + texture: WebGLTexture, + data: CompressedData, +) { + const { glInternalFormat, mipmaps, width, height } = data; + + glw.bindTexture(texture); + + let w = width; + let h = height; + + for (let i = 0; i < mipmaps.length; i++) { + glw.compressedTexImage2D( + i, + glInternalFormat, + w, + h, + 0, + new Uint8Array(mipmaps[i]!), + ); + + w = Math.max(1, w >> 1); + h = Math.max(1, h >> 1); } -} + + glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); + glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); + glw.texParameteri( + glw.TEXTURE_MIN_FILTER, + mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR, + ); +}; + +type BlockInfo = { + width: number; + height: number; + bytes: number; +}; + +// Predefined block info for common compressed texture formats +const BLOCK_4x4x8: BlockInfo = { width: 4, height: 4, bytes: 8 }; +const BLOCK_4x4x16: BlockInfo = { width: 4, height: 4, bytes: 16 }; +const BLOCK_5x5x16: BlockInfo = { width: 5, height: 5, bytes: 16 }; +const BLOCK_6x6x16: BlockInfo = { width: 6, height: 6, bytes: 16 }; +const BLOCK_8x4x8: BlockInfo = { width: 8, height: 4, bytes: 8 }; +const BLOCK_8x8x16: BlockInfo = { width: 8, height: 8, bytes: 16 }; +const BLOCK_10x10x16: BlockInfo = { width: 10, height: 10, bytes: 16 }; +const BLOCK_12x12x16: BlockInfo = { width: 12, height: 12, bytes: 16 }; + +// Map of GL internal formats to their corresponding block info +export const blockInfoMap: { [key: number]: BlockInfo } = { + // S3TC / DXTn (WEBGL_compressed_texture_s3tc, sRGB variants) + 0x83f0: BLOCK_4x4x8, // COMPRESSED_RGB_S3TC_DXT1_EXT + 0x83f1: BLOCK_4x4x8, // COMPRESSED_RGBA_S3TC_DXT1_EXT + 0x83f2: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT3_EXT + 0x83f3: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT5_EXT + + // ETC1 / ETC2 / EAC + 0x8d64: BLOCK_4x4x8, // COMPRESSED_RGB_ETC1_WEBGL + 0x9274: BLOCK_4x4x8, // COMPRESSED_RGB8_ETC2 + 0x9275: BLOCK_4x4x8, // COMPRESSED_SRGB8_ETC2 + 0x9278: BLOCK_4x4x16, // COMPRESSED_RGBA8_ETC2_EAC + 0x9279: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC + + // PVRTC (WEBGL_compressed_texture_pvrtc) + 0x8c00: BLOCK_4x4x8, // COMPRESSED_RGB_PVRTC_4BPPV1_IMG + 0x8c02: BLOCK_4x4x8, // COMPRESSED_RGBA_PVRTC_4BPPV1_IMG + 0x8c01: BLOCK_8x4x8, // COMPRESSED_RGB_PVRTC_2BPPV1_IMG + 0x8c03: BLOCK_8x4x8, + + // ASTC (WEBGL_compressed_texture_astc) + 0x93b0: BLOCK_4x4x16, // COMPRESSED_RGBA_ASTC_4x4_KHR + 0x93d0: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR + 0x93b1: BLOCK_5x5x16, // 5x5 + 0x93d1: BLOCK_5x5x16, + 0x93b2: BLOCK_6x6x16, // 6x6 + 0x93d2: BLOCK_6x6x16, + 0x93b3: BLOCK_8x8x16, // 8x8 + 0x93d3: BLOCK_8x8x16, + 0x93b4: BLOCK_10x10x16, // 10x10 + 0x93d4: BLOCK_10x10x16, + 0x93b5: BLOCK_12x12x16, // 12x12 + 0x93d5: BLOCK_12x12x16, +}; + +export const uploadCompressedTexture: Record< + string, + UploadCompressedTextureFunction +> = { + ktx: uploadKTX, + pvr: uploadPVR, + astc: uploadASTC, +}; diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index d0ea3b87..b0ce7da3 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -21,6 +21,7 @@ import type { Dimensions } from '../../../common/CommonTypes.js'; import { assertTruthy } from '../../../utils.js'; import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; +import { uploadCompressedTexture } from '../../lib/textureCompression.js'; import type { Texture } from '../../textures/Texture.js'; import { CoreContextTexture } from '../CoreContextTexture.js'; import { isHTMLImageElement } from './internal/RendererUtils.js'; @@ -216,56 +217,8 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { this.setTextureMemUse(height * width * formatBytes * memoryPadding); } else if (tdata && 'mipmaps' in tdata && tdata.mipmaps) { - const { mipmaps, type, glInternalFormat, blockInfo } = tdata; - - let w = tdata.width; - let h = tdata.height; - - glw.bindTexture(this._nativeCtxTexture); - - const blockWidth = blockInfo.width; - const blockHeight = blockInfo.height; - - for (let i = 0; i < mipmaps.length; i++) { - let view = - type === 'ktx' - ? new Uint8Array(mipmaps[i] ?? new ArrayBuffer(0)) - : new Uint8Array( - (mipmaps[i] as unknown as ArrayBufferView).buffer, - (mipmaps[i] as unknown as ArrayBufferView).byteOffset, - (mipmaps[i] as unknown as ArrayBufferView).byteLength, - ); - - const uploadW = Math.ceil(w / blockWidth) * blockWidth; - const uploadH = Math.ceil(h / blockHeight) * blockHeight; - - const expectedBytes = - Math.ceil(w / blockWidth) * - Math.ceil(h / blockHeight) * - blockInfo.bytes; - - if (view.byteLength < expectedBytes) { - const padded = new Uint8Array(expectedBytes); - padded.set(view); - view = padded; - } - glw.compressedTexImage2D( - i, - glInternalFormat, - uploadW, - uploadH, - 0, - view, - ); - - w = Math.max(1, w >> 1); - h = Math.max(1, h >> 1); - } - - glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); - glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); - glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); - glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); + const { mipmaps, type, blockInfo } = tdata; + uploadCompressedTexture[type]!(glw, this._nativeCtxTexture, tdata); // Check for errors after compressed texture operations if (this.checkGLError() === true) { diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index e0af237a..b073b2be 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -355,11 +355,11 @@ export class ImageTexture extends Texture { } if (type === 'compressed') { - return loadCompressedTexture(absoluteSrc, this.props); + return loadCompressedTexture(absoluteSrc); } if (isCompressedTextureContainer(this.props) === true) { - return loadCompressedTexture(absoluteSrc, this.props); + return loadCompressedTexture(absoluteSrc); } // default diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index ec5795f1..a5c6b8ca 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -45,7 +45,7 @@ export type TextureLoadedEventHandler = ( /** * Represents compressed texture data. */ -interface CompressedData { +export interface CompressedData { /** * GLenum spcifying compression format */ @@ -54,12 +54,12 @@ interface CompressedData { /** * All mipmap levels */ - mipmaps?: ArrayBuffer[]; + mipmaps: ArrayBuffer[]; /** - * Supported container types ('pvr' or 'ktx'). + * Supported container types ('pvr', 'ktx', 'astc'). */ - type: 'pvr' | 'ktx'; + type: 'pvr' | 'ktx' | 'astc'; /** * The width of the compressed texture in pixels. Defaults to 0. From 05c18d9615215f69e5c5bfd5a561f3b4c9024316 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Mon, 17 Nov 2025 14:27:15 +0100 Subject: [PATCH 6/9] fix subtextures --- examples/tests/tx-compression.ts | 33 ++++++++++--------- src/core/lib/textureCompression.ts | 23 ++++++++++--- src/core/renderers/webgl/WebGlCoreRenderer.ts | 23 ++++++------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index d4e00704..06016d29 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -20,7 +20,7 @@ import type { ExampleSettings } from '../common/ExampleSettings.js'; export default async function ({ renderer, testRoot }: ExampleSettings) { - const ktx1Label = renderer.createTextNode({ + const cn1Label = renderer.createTextNode({ x: 100, y: 100, color: 0xffffffff, @@ -31,33 +31,37 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - const url1 = - 'https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=400&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast'; + const url1 = `http://localhost:8080/?url=https%3A%2F%2Fimg001-eu-mo-prd.delivery.skycdp.com%2Fselect%2Fimage%3FentityId%3D8557447259811357112%26companyId%3D5598815685921580105%26width%3D1920%26ratio%3D16x9%26rule%3DSky_Tile%26partnerIdentifier%3Dsky-uk%26location%3DGB%26outputFormat%3Dpng&actions=(a:resize,t:inside,w:${ + 123 * 4 + })&output=(f:ktx,c:etc,t:ETC1,q:etcfast)`; const randomWidth = Math.floor(Math.random() * 300) + 200; - const url2 = `https://img001-eu-mo-prd.delivery.skycdp.com/select/image?entityId=8557447259811357112&companyId=5598815685921580105&width=${randomWidth}&ratio=16x9&rule=Sky_Tile&partnerIdentifier=sky-uk&location=GB&route=lightning&outputFormat=ktx&compression=etc&compressionType=ETC1&compressionQuality=etcfast`; + const url2 = `http://localhost:8080/?url=https%3A%2F%2Fimg001-eu-mo-prd.delivery.skycdp.com%2Fselect%2Fimage%3FentityId%3D8557447259811357112%26companyId%3D5598815685921580105%26width%3D1920%26ratio%3D16x9%26rule%3DSky_Tile%26partnerIdentifier%3Dsky-uk%26location%3DGB%26outputFormat%3Dpng&actions=(a:resize,t:inside,w:${randomWidth})&output=(f:ktx,c:etc,t:ETC1,q:etcfast)`; - const ktx1 = renderer.createNode({ + const compressedNode1 = renderer.createNode({ x: 100, y: 170, width: 550, height: 550, - // src: '../assets/test-etc1.pvr', + // src: '../assets/green-25.png', src: url1, imageType: 'compressed', parent: testRoot, }); - ktx1.on('loaded', (node, data) => { - console.log('ktx1 loaded'); + compressedNode1.on('loaded', (node, data) => { const { width, height } = data.dimensions; node.width = width; node.height = height; - ktx1Label.text = `w: ${width}, h: ${height}`; + cn1Label.text = `w: ${width}, h: ${height}`; }); - const ktx2Label = renderer.createTextNode({ + compressedNode1.on('failed', (node, error) => { + console.error('compressed error', error); + }); + + const cn2Label = renderer.createTextNode({ x: 800, y: 100, color: 0xffffffff, @@ -68,21 +72,20 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - const ktx2 = renderer.createNode({ + const compressedNode2 = renderer.createNode({ x: 800, y: 170, width: 400, height: 400, src: url2, - imageType: 'ktx', + imageType: 'compressed', parent: testRoot, }); - ktx2.on('loaded', (node, data) => { - console.log('ktx2 loaded'); + compressedNode2.on('loaded', (node, data) => { const { width, height } = data.dimensions; node.width = width; node.height = height; - ktx2Label.text = `w: ${width}, h: ${height}`; + cn2Label.text = `w: ${width}, h: ${height}`; }); } diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index f7bd6562..cb5455a4 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -88,11 +88,18 @@ export const loadCompressedTexture = async ( } const arrayBuffer = await response.arrayBuffer(); + + // Ensure we have enough data to check magic numbers + if (arrayBuffer.byteLength < 16) { + throw new Error( + `File too small to be a valid compressed texture (${arrayBuffer.byteLength} bytes). Expected at least 16 bytes for header inspection.`, + ); + } + const view = new DataView(arrayBuffer); const magic = view.getUint32(0, true); - console.log('magic', magic); + if (magic === PVR_MAGIC) { - console.log('pvr'); return loadPVR(view); } @@ -100,12 +107,20 @@ export const loadCompressedTexture = async ( return loadASTC(view); } + let isKTX = true; + for (let i = 0; i < KTX_IDENTIFIER.length; i++) { if (view.getUint8(i) !== KTX_IDENTIFIER[i]) { - throw new Error('Unrecognized compressed texture format'); + isKTX = false; + break; } } - return loadKTX(view); + + if (isKTX === true) { + return loadKTX(view); + } else { + throw new Error('Unrecognized compressed texture format'); + } } catch (error) { throw new Error(`Failed to load compressed texture from ${url}: ${error}`); } diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index 9dc41a36..35ac380d 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -269,18 +269,12 @@ export class WebGlCoreRenderer extends CoreRenderer { assertTruthy(curRenderOp); } - let ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - if (ctxTexture === undefined) { - ctxTexture = this.stage.defaultTexture?.ctxTexture as WebGlCoreCtxTexture; - console.warn( - 'WebGL Renderer: Texture does not have a ctxTexture, using default texture instead', - ); - } + let ctxTexture = undefined; - let texCoordX1 = ctxTexture.txCoordX1; - let texCoordY1 = ctxTexture.txCoordY1; - let texCoordX2 = ctxTexture.txCoordX2; - let texCoordY2 = ctxTexture.txCoordY2; + let texCoordX1 = 0; + let texCoordY1 = 0; + let texCoordX2 = 1; + let texCoordY2 = 1; if (texture.type === TextureType.subTexture) { const { @@ -297,6 +291,13 @@ export class WebGlCoreRenderer extends CoreRenderer { texCoordY1 = ty / parentH; texCoordY2 = texCoordY1 + th / parentH; texture = (texture as SubTexture).parentTexture; + ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; + } else { + ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; + texCoordX1 = ctxTexture.txCoordX1; + texCoordY1 = ctxTexture.txCoordY1; + texCoordX2 = ctxTexture.txCoordX2; + texCoordY2 = ctxTexture.txCoordY2; } if ( From d07c18d0ac73fd75fc7d2b15c2af223cab9bc446 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 18 Nov 2025 14:59:31 +0100 Subject: [PATCH 7/9] cleanup --- examples/tests/tx-compression.ts | 12 ++---------- src/core/lib/textureCompression.ts | 3 --- src/core/textures/ImageTexture.ts | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index 06016d29..699d8a47 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -31,20 +31,12 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); - const url1 = `http://localhost:8080/?url=https%3A%2F%2Fimg001-eu-mo-prd.delivery.skycdp.com%2Fselect%2Fimage%3FentityId%3D8557447259811357112%26companyId%3D5598815685921580105%26width%3D1920%26ratio%3D16x9%26rule%3DSky_Tile%26partnerIdentifier%3Dsky-uk%26location%3DGB%26outputFormat%3Dpng&actions=(a:resize,t:inside,w:${ - 123 * 4 - })&output=(f:ktx,c:etc,t:ETC1,q:etcfast)`; - - const randomWidth = Math.floor(Math.random() * 300) + 200; - const url2 = `http://localhost:8080/?url=https%3A%2F%2Fimg001-eu-mo-prd.delivery.skycdp.com%2Fselect%2Fimage%3FentityId%3D8557447259811357112%26companyId%3D5598815685921580105%26width%3D1920%26ratio%3D16x9%26rule%3DSky_Tile%26partnerIdentifier%3Dsky-uk%26location%3DGB%26outputFormat%3Dpng&actions=(a:resize,t:inside,w:${randomWidth})&output=(f:ktx,c:etc,t:ETC1,q:etcfast)`; - const compressedNode1 = renderer.createNode({ x: 100, y: 170, width: 550, height: 550, - // src: '../assets/green-25.png', - src: url1, + src: '../assets/test-etc1.pvr', imageType: 'compressed', parent: testRoot, }); @@ -77,7 +69,7 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { y: 170, width: 400, height: 400, - src: url2, + src: '../assets/test-s3tc.ktx', imageType: 'compressed', parent: testRoot, }); diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index cb5455a4..057b3a55 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -37,9 +37,6 @@ export type UploadCompressedTextureFunction = ( export function isCompressedTextureContainer( props: ImageTextureProps, ): boolean { - if (props.type === 'ktx' || props.type === 'pvr') { - return true; - } return /\.(ktx|pvr)$/.test(props.src as string); } diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index b073b2be..8b822aef 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -76,7 +76,7 @@ export interface ImageTextureProps { * * @default null */ - type?: 'regular' | 'compressed' | 'pvr' | 'ktx' | 'svg' | null; + type?: 'regular' | 'compressed' | 'svg' | null; /** * The width of the rectangle from which the ImageBitmap will be extracted. This value * can be negative. Only works when createImageBitmap is supported on the browser. From 8a14da749a27a8e58896669a17105550259f4957 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 19 Nov 2025 15:30:28 +0100 Subject: [PATCH 8/9] remove unneeded properties textureOptions --- src/core/CoreTextureManager.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index b5ed2578..7bdc23e9 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -176,21 +176,6 @@ export interface TextureOptions { * The resize modes cover and contain are supported */ resizeMode?: ResizeModeOptions; - - /** - * Width info used for compressed textures. Otherwise falls back to node width. - */ - width?: number; - - /** - * Height info used for compressed textures. Otherwise falls back to node height. - */ - height?: number; - - /** - * Chunk size VxV, default value is 1. Used for Compressed textures - */ - chunkSize?: number; } export class CoreTextureManager extends EventEmitter { From 9d1e75638cb30b5d3758864cd1f1bacda861b7d2 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 19 Nov 2025 16:48:50 +0100 Subject: [PATCH 9/9] fixed hidden typescript errors --- src/core/lib/textureCompression.ts | 28 +++++++++++----------------- src/core/textures/ImageTexture.ts | 2 +- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/core/lib/textureCompression.ts b/src/core/lib/textureCompression.ts index 057b3a55..48ab4914 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -16,7 +16,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { ImageTextureProps } from '../textures/ImageTexture.js'; import { type CompressedData, type TextureData } from '../textures/Texture.js'; import type { WebGlContextWrapper } from './WebGlContextWrapper.js'; @@ -34,10 +33,8 @@ export type UploadCompressedTextureFunction = ( * and only supports the following extensions: .ktx and .pvr * @returns */ -export function isCompressedTextureContainer( - props: ImageTextureProps, -): boolean { - return /\.(ktx|pvr)$/.test(props.src as string); +export function isCompressedTextureContainer(src: string): boolean { + return /\.(ktx|pvr)$/.test(src); } const PVR_MAGIC = 0x03525650; // 'PVR3' in little-endian @@ -136,9 +133,7 @@ function readUint24(view: DataView, offset: number) { * @param view * @returns */ -const loadASTC = async function ( - view: DataView, -): Promise { +const loadASTC = async function (view: DataView): Promise { const blockX = view.getUint8(4); const blockY = view.getUint8(5); const sizeX = readUint24(view, 7); @@ -160,8 +155,10 @@ const loadASTC = async function ( throw new Error(`Unsupported ASTC block size: ${blockX}x${blockY}`); } + const buffer = view.buffer as ArrayBuffer; + const mipmaps: ArrayBuffer[] = []; - mipmaps.push(view.buffer.slice(16)); + mipmaps.push(buffer.slice(16)); return { data: { @@ -204,9 +201,7 @@ const uploadASTC = function ( * @param view * @returns */ -const loadKTX = async function ( - view: DataView, -): Promise { +const loadKTX = async function (view: DataView): Promise { const endianness = view.getUint32(12, true); const littleEndian = endianness === 0x04030201; if (littleEndian === false && endianness !== 0x01020304) { @@ -243,6 +238,7 @@ const loadKTX = async function ( const bytesOfKeyValueData = view.getUint32(60, littleEndian); const mipmaps: ArrayBuffer[] = []; + const buffer = view.buffer as ArrayBuffer; let offset = 64 + bytesOfKeyValueData; if (offset > view.byteLength) { @@ -255,7 +251,7 @@ const loadKTX = async function ( const end = offset + imageSize; - mipmaps.push(view.buffer.slice(offset, end)); + mipmaps.push(buffer.slice(offset, end)); offset = end; if (offset % 4 !== 0) { offset += 4 - (offset % 4); @@ -327,9 +323,7 @@ function pvrtcMipSize(width: number, height: number, bpp: 2 | 4) { return (w * h * bpp) / 8; } -const loadPVR = async function ( - view: DataView, -): Promise { +const loadPVR = async function (view: DataView): Promise { const pixelFormatLow = view.getUint32(8, true); const internalFormat = PVR_TO_GL_INTERNAL_FORMAT[pixelFormatLow]; @@ -348,7 +342,7 @@ const loadPVR = async function ( } const mipmapLevels = view.getInt32(44, true); const metadataSize = view.getUint32(48, true); - const buffer = view.buffer; + const buffer = view.buffer as ArrayBuffer; let offset = 52 + metadataSize; if (offset > buffer.byteLength) { diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 8b822aef..35d2ee58 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -358,7 +358,7 @@ export class ImageTexture extends Texture { return loadCompressedTexture(absoluteSrc); } - if (isCompressedTextureContainer(this.props) === true) { + if (isCompressedTextureContainer(src) === true) { return loadCompressedTexture(absoluteSrc); }