diff --git a/examples/tests/tx-compression.ts b/examples/tests/tx-compression.ts index 8031c850..699d8a47 100644 --- a/examples/tests/tx-compression.ts +++ b/examples/tests/tx-compression.ts @@ -20,43 +20,64 @@ import type { ExampleSettings } from '../common/ExampleSettings.js'; export default async function ({ renderer, testRoot }: ExampleSettings) { - renderer.createTextNode({ + const cn1Label = renderer.createTextNode({ x: 100, y: 100, color: 0xffffffff, alpha: 1.0, - text: 'etc1 compression in .pvr', + text: 'w: 400', fontFamily: 'Ubuntu', fontSize: 30, parent: testRoot, }); - renderer.createNode({ + const compressedNode1 = renderer.createNode({ x: 100, y: 170, width: 550, height: 550, src: '../assets/test-etc1.pvr', + imageType: 'compressed', parent: testRoot, }); - renderer.createTextNode({ + compressedNode1.on('loaded', (node, data) => { + const { width, height } = data.dimensions; + node.width = width; + node.height = height; + + cn1Label.text = `w: ${width}, h: ${height}`; + }); + + compressedNode1.on('failed', (node, error) => { + console.error('compressed error', error); + }); + + const cn2Label = renderer.createTextNode({ x: 800, y: 100, color: 0xffffffff, alpha: 1.0, - text: 's3tc compression in .ktx', + text: 'w: 333', fontFamily: 'Ubuntu', fontSize: 30, parent: testRoot, }); - renderer.createNode({ + const compressedNode2 = renderer.createNode({ x: 800, y: 170, width: 400, height: 400, src: '../assets/test-s3tc.ktx', + imageType: 'compressed', parent: testRoot, }); + + compressedNode2.on('loaded', (node, data) => { + const { width, height } = data.dimensions; + node.width = width; + node.height = height; + cn2Label.text = `w: ${width}, h: ${height}`; + }); } 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 6b3b31e5..48ab4914 100644 --- a/src/core/lib/textureCompression.ts +++ b/src/core/lib/textureCompression.ts @@ -16,8 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { type CompressedData, type TextureData } from '../textures/Texture.js'; +import type { WebGlContextWrapper } from './WebGlContextWrapper.js'; -import { type TextureData } from '../textures/Texture.js'; +export type UploadCompressedTextureFunction = ( + glw: WebGlContextWrapper, + texture: WebGLTexture, + data: CompressedData, +) => void; /** * Tests if the given location is a compressed texture container @@ -27,10 +33,38 @@ 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(src: string): boolean { + return /\.(ktx|pvr)$/.test(src); } +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 @@ -41,7 +75,6 @@ export const loadCompressedTexture = async ( ): Promise => { try { const response = await fetch(url); - if (!response.ok) { throw new Error( `Failed to fetch compressed texture: ${response.status} ${response.statusText}`, @@ -50,114 +83,422 @@ export const loadCompressedTexture = async ( const arrayBuffer = await response.arrayBuffer(); - if (url.indexOf('.ktx') !== -1) { - return loadKTXData(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); + + if (magic === PVR_MAGIC) { + return loadPVR(view); + } + + if (magic === ASTC_MAGIC) { + return loadASTC(view); + } + + let isKTX = true; + + for (let i = 0; i < KTX_IDENTIFIER.length; i++) { + if (view.getUint8(i) !== KTX_IDENTIFIER[i]) { + isKTX = false; + break; + } } - return loadPVRData(arrayBuffer); + 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}`); } }; +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); - let offset = 64; + 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}`, + ); + } - // 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; + const internalFormat = ASTC_TO_GL_INTERNAL_FORMAT[`${blockX}x${blockY}`]; + if (internalFormat === undefined) { + throw new Error(`Unsupported ASTC block size: ${blockX}x${blockY}`); + } - for (let i = 0; i < data.numberOfMipmapLevels; i++) { - const imageSize = view.getUint32(offset); - offset += 4; + const buffer = view.buffer as ArrayBuffer; - mipmaps.push(view.buffer.slice(offset, imageSize)); - offset += imageSize; - } + const mipmaps: ArrayBuffer[] = []; + mipmaps.push(buffer.slice(16)); return { data: { - glInternalFormat: data.glInternalFormat, + blockInfo: blockInfoMap[internalFormat]!, + glInternalFormat: internalFormat, mipmaps, - width: data.pixelWidth || 0, - height: data.pixelHeight || 0, - type: 'ktx', + 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 PVR texture container and returns the texture data - * @param buffer + * Loads a KTX texture container and returns the texture data + * @param view * @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 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'); + } + + 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})`, + ); + } + + 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[] = []; + const buffer = view.buffer as 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; + + const end = offset + imageSize; + + mipmaps.push(buffer.slice(offset, end)); + offset = end; + if (offset % 4 !== 0) { + offset += 4 - (offset % 4); + } + } + + return { + data: { + blockInfo: blockInfoMap[glInternalFormat]!, + glInternalFormat: glInternalFormat, + mipmaps, + width: width, + height: height, + type: 'ktx', + }, + premultiplyAlpha: false, }; +}; + +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; - let offset = 0; - let width = data.pixelWidth || 0; - let height = data.pixelHeight || 0; + 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); + } - 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, + 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; +} + +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); - mipmaps.push(view); - offset += level; - width = width >> 1; - height = height >> 1; + // 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 as ArrayBuffer; + + 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; + } + + 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: { - 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, }; }; + +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 b9da6038..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'; @@ -43,6 +44,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,26 +217,22 @@ 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 view = - type === 'ktx' - ? new DataView(mipmaps[0] ?? new ArrayBuffer(0)) - : (mipmaps[0] as unknown as ArrayBufferView); - - glw.bindTexture(this._nativeCtxTexture); - - glw.compressedTexImage2D(0, glInternalFormat, width, height, 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); - 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) { return { width: 0, height: 0 }; } - this.setTextureMemUse(view.byteLength); + 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 width = 1; diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index fcc0b38a..35ac380d 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -269,6 +269,8 @@ export class WebGlCoreRenderer extends CoreRenderer { assertTruthy(curRenderOp); } + let ctxTexture = undefined; + let texCoordX1 = 0; let texCoordY1 = 0; let texCoordX2 = 1; @@ -289,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 ( @@ -335,14 +344,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/Texture.ts b/src/core/textures/Texture.ts index af4ec87c..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. @@ -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