UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

408 lines (330 loc) 15.2 kB
var KTXParser = require('../../../src/textures/parsers/KTXParser'); // KTX file identifier: 12 magic bytes var KTX_IDENTIFIER = [ 0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a ]; // Build a valid KTX ArrayBuffer for testing. // Header layout (offsets are absolute in the buffer): // 0-11: file identifier // 12-15: endianness marker (0x04030201 = little-endian) // 16-19: glType (must be 0 for compressed) // 20-23: glTypeSize // 24-27: glFormat // 28-31: internalFormat (head offset 16 = 4 * size) // 32-35: glBaseInternalFormat // 36-39: pixelWidth (head offset 24 = 6 * size) // 40-43: pixelHeight (head offset 28 = 7 * size) // 44-47: pixelDepth // 48-51: numberOfArrayElements // 52-55: numberOfFaces // 56-59: numberOfMipmapLevels (head offset 44 = 11 * size) // 60-63: bytesOfKeyValueData (head offset 48 = 12 * size) // 64+: key-value data, then mipmap blocks function buildKTXBuffer(options) { var width = options.width !== undefined ? options.width : 4; var height = options.height !== undefined ? options.height : 4; var internalFormat = options.internalFormat !== undefined ? options.internalFormat : 0x83F0; var mipmapLevels = options.mipmapLevels !== undefined ? options.mipmapLevels : 1; var bytesOfKeyValueData = options.bytesOfKeyValueData !== undefined ? options.bytesOfKeyValueData : 0; var glType = options.glType !== undefined ? options.glType : 0; var littleEndian = options.littleEndian !== undefined ? options.littleEndian : true; var badIdentifier = options.badIdentifier || false; // Calculate level sizes (use width*height as a simple proxy for compressed data size). // Always generate at least 1 level of data so that a header value of 0 (which the // parser clamps to 1 via Math.max) still finds readable bytes in the buffer. var levelSizes = []; var lw = width; var lh = height; var i; var levelsToWrite = Math.max(1, mipmapLevels); for (i = 0; i < levelsToWrite; i++) { // Use at least 4 bytes so Int32Array alignment is satisfied var levelDataSize = Math.max(4, lw * lh); levelSizes.push(levelDataSize); lw = Math.max(1, lw >> 1); lh = Math.max(1, lh >> 1); } var mipmapByteTotal = 0; for (i = 0; i < levelSizes.length; i++) { mipmapByteTotal += 4 + levelSizes[i]; // 4-byte size field + data } var totalSize = 64 + bytesOfKeyValueData + mipmapByteTotal; var buffer = new ArrayBuffer(totalSize); var view = new DataView(buffer); var bytes = new Uint8Array(buffer); // Write identifier for (i = 0; i < 12; i++) { bytes[i] = badIdentifier ? 0x00 : KTX_IDENTIFIER[i]; } // Write header fields using DataView at absolute offsets. // The endianness marker is always the value 0x04030201 stored in the file's // own byte order. The parser reads it as little-endian and checks whether the // result equals 0x04030201: if the file is big-endian the bytes are reversed, // so the LE read returns 0x01020304 → parser correctly sets littleEndian=false. view.setUint32(12, 0x04030201, littleEndian); view.setUint32(16, glType, littleEndian); // glType view.setUint32(20, 1, littleEndian); // glTypeSize view.setUint32(24, 0, littleEndian); // glFormat view.setUint32(28, internalFormat, littleEndian);// internalFormat view.setUint32(32, 0, littleEndian); // glBaseInternalFormat view.setUint32(36, width, littleEndian); // pixelWidth view.setUint32(40, height, littleEndian); // pixelHeight view.setUint32(44, 0, littleEndian); // pixelDepth view.setUint32(48, 0, littleEndian); // numberOfArrayElements view.setUint32(52, 1, littleEndian); // numberOfFaces view.setUint32(56, mipmapLevels, littleEndian); // numberOfMipmapLevels view.setUint32(60, bytesOfKeyValueData, littleEndian); // bytesOfKeyValueData // Write mipmap blocks (key-value data is left as zeroes) var offset = 64 + bytesOfKeyValueData; for (i = 0; i < levelSizes.length; i++) { // The KTX parser reads levelSize via native Int32Array (always little-endian // on x86), so we must always write the size field in little-endian regardless // of the KTX endianness flag stored in the header. view.setInt32(offset, levelSizes[i], true); offset += 4; // Fill level data with a recognisable byte pattern for (var b = 0; b < levelSizes[i]; b++) { bytes[offset + b] = (i + 1) & 0xff; } offset += levelSizes[i]; } return buffer; } describe('Phaser.Textures.Parsers.KTXParser', function () { beforeEach(function () { vi.spyOn(console, 'warn').mockImplementation(function () {}); }); afterEach(function () { vi.restoreAllMocks(); }); // ------------------------------------------------------------------------- // File identifier validation // ------------------------------------------------------------------------- it('should return undefined when the file identifier is invalid', function () { var buffer = buildKTXBuffer({ badIdentifier: true }); var result = KTXParser(buffer); expect(result).toBeUndefined(); }); it('should warn when the file identifier is invalid', function () { var buffer = buildKTXBuffer({ badIdentifier: true }); KTXParser(buffer); expect(console.warn).toHaveBeenCalledWith('KTXParser - Invalid file format'); }); it('should return undefined when only the first identifier byte is wrong', function () { var buffer = buildKTXBuffer({}); var bytes = new Uint8Array(buffer); bytes[0] = 0x00; var result = KTXParser(buffer); expect(result).toBeUndefined(); }); it('should return undefined when only the last identifier byte is wrong', function () { var buffer = buildKTXBuffer({}); var bytes = new Uint8Array(buffer); bytes[11] = 0x00; var result = KTXParser(buffer); expect(result).toBeUndefined(); }); // ------------------------------------------------------------------------- // Compressed-only validation // ------------------------------------------------------------------------- it('should return undefined when glType is non-zero (uncompressed format)', function () { var buffer = buildKTXBuffer({ glType: 1 }); var result = KTXParser(buffer); expect(result).toBeUndefined(); }); it('should warn when glType is non-zero', function () { var buffer = buildKTXBuffer({ glType: 5 }); KTXParser(buffer); expect(console.warn).toHaveBeenCalledWith('KTXParser - Only compressed formats supported'); }); // ------------------------------------------------------------------------- // Successful parse — return structure // ------------------------------------------------------------------------- it('should return an object with the expected shape for a valid KTX buffer', function () { var buffer = buildKTXBuffer({ width: 4, height: 4 }); var result = KTXParser(buffer); expect(result).toBeDefined(); expect(typeof result).toBe('object'); expect(Array.isArray(result.mipmaps)).toBe(true); expect(typeof result.width).toBe('number'); expect(typeof result.height).toBe('number'); expect(typeof result.internalFormat).toBe('number'); expect(typeof result.compressed).toBe('boolean'); expect(typeof result.generateMipmap).toBe('boolean'); }); it('should set compressed to true', function () { var buffer = buildKTXBuffer({ width: 4, height: 4 }); var result = KTXParser(buffer); expect(result.compressed).toBe(true); }); it('should set generateMipmap to false', function () { var buffer = buildKTXBuffer({ width: 4, height: 4 }); var result = KTXParser(buffer); expect(result.generateMipmap).toBe(false); }); // ------------------------------------------------------------------------- // Dimensions // ------------------------------------------------------------------------- it('should parse width and height correctly', function () { var buffer = buildKTXBuffer({ width: 128, height: 64 }); var result = KTXParser(buffer); expect(result.width).toBe(128); expect(result.height).toBe(64); }); it('should parse a 1x1 texture correctly', function () { var buffer = buildKTXBuffer({ width: 1, height: 1 }); var result = KTXParser(buffer); expect(result.width).toBe(1); expect(result.height).toBe(1); }); it('should parse a non-square texture correctly', function () { var buffer = buildKTXBuffer({ width: 256, height: 512 }); var result = KTXParser(buffer); expect(result.width).toBe(256); expect(result.height).toBe(512); }); // ------------------------------------------------------------------------- // Internal format // ------------------------------------------------------------------------- it('should parse internalFormat correctly', function () { var buffer = buildKTXBuffer({ internalFormat: 0x83F0 }); var result = KTXParser(buffer); expect(result.internalFormat).toBe(0x83F0); }); it('should parse a different internalFormat value correctly', function () { var buffer = buildKTXBuffer({ internalFormat: 0x9274 }); var result = KTXParser(buffer); expect(result.internalFormat).toBe(0x9274); }); // ------------------------------------------------------------------------- // Mipmap levels // ------------------------------------------------------------------------- it('should return a single mipmap when mipmapLevels is 1', function () { var buffer = buildKTXBuffer({ width: 4, height: 4, mipmapLevels: 1 }); var result = KTXParser(buffer); expect(result.mipmaps.length).toBe(1); }); it('should return multiple mipmaps when mipmapLevels is greater than 1', function () { var buffer = buildKTXBuffer({ width: 8, height: 8, mipmapLevels: 4 }); var result = KTXParser(buffer); expect(result.mipmaps.length).toBe(4); }); it('should clamp mipmapLevels to at least 1 when header value is 0', function () { var buffer = buildKTXBuffer({ width: 4, height: 4, mipmapLevels: 0 }); // buildKTXBuffer writes 0 for the level count; parser does Math.max(1, ...) var result = KTXParser(buffer); expect(result.mipmaps.length).toBe(1); }); it('should give each mipmap a data property that is a Uint8Array', function () { var buffer = buildKTXBuffer({ width: 4, height: 4, mipmapLevels: 3 }); var result = KTXParser(buffer); for (var i = 0; i < result.mipmaps.length; i++) { expect(result.mipmaps[i].data).toBeInstanceOf(Uint8Array); } }); it('should give the first mipmap the full image dimensions', function () { var buffer = buildKTXBuffer({ width: 8, height: 8, mipmapLevels: 4 }); var result = KTXParser(buffer); expect(result.mipmaps[0].width).toBe(8); expect(result.mipmaps[0].height).toBe(8); }); it('should halve mipmap dimensions at each level', function () { var buffer = buildKTXBuffer({ width: 8, height: 8, mipmapLevels: 4 }); var result = KTXParser(buffer); expect(result.mipmaps[0].width).toBe(8); expect(result.mipmaps[0].height).toBe(8); expect(result.mipmaps[1].width).toBe(4); expect(result.mipmaps[1].height).toBe(4); expect(result.mipmaps[2].width).toBe(2); expect(result.mipmaps[2].height).toBe(2); expect(result.mipmaps[3].width).toBe(1); expect(result.mipmaps[3].height).toBe(1); }); it('should not let mipmap dimensions fall below 1', function () { // 1x1 with 3 declared mipmap levels — dimensions must stay at 1 var buffer = buildKTXBuffer({ width: 1, height: 1, mipmapLevels: 3 }); var result = KTXParser(buffer); for (var i = 0; i < result.mipmaps.length; i++) { expect(result.mipmaps[i].width).toBeGreaterThanOrEqual(1); expect(result.mipmaps[i].height).toBeGreaterThanOrEqual(1); } }); it('should not let mipmap dimensions fall below 1 for asymmetric textures', function () { var buffer = buildKTXBuffer({ width: 2, height: 4, mipmapLevels: 3 }); var result = KTXParser(buffer); expect(result.mipmaps[2].width).toBe(1); expect(result.mipmaps[2].height).toBe(1); }); // ------------------------------------------------------------------------- // Endianness // ------------------------------------------------------------------------- it('should parse a big-endian KTX file correctly', function () { var buffer = buildKTXBuffer({ width: 4, height: 4, littleEndian: false, internalFormat: 0x8C00 }); var result = KTXParser(buffer); expect(result.width).toBe(4); expect(result.height).toBe(4); expect(result.internalFormat).toBe(0x8C00); }); // ------------------------------------------------------------------------- // bytesOfKeyValueData offset handling // ------------------------------------------------------------------------- it('should correctly skip key-value data before reading mipmaps', function () { // 8 bytes of extra key-value data (must be a multiple of 4 for alignment) var buffer = buildKTXBuffer({ width: 4, height: 4, bytesOfKeyValueData: 8 }); var result = KTXParser(buffer); expect(result).toBeDefined(); expect(result.mipmaps.length).toBe(1); expect(result.mipmaps[0].data).toBeInstanceOf(Uint8Array); }); // ------------------------------------------------------------------------- // Mipmap data content // ------------------------------------------------------------------------- it('should expose mipmap data that reads back from the original buffer', function () { var buffer = buildKTXBuffer({ width: 4, height: 4, mipmapLevels: 1 }); var result = KTXParser(buffer); // buildKTXBuffer fills level data with (levelIndex + 1) & 0xff = 1 expect(result.mipmaps[0].data[0]).toBe(1); }); it('should expose distinct mipmap data for each level', function () { var buffer = buildKTXBuffer({ width: 8, height: 8, mipmapLevels: 2 }); var result = KTXParser(buffer); // Level 0 bytes are filled with 1, level 1 bytes with 2 expect(result.mipmaps[0].data[0]).toBe(1); expect(result.mipmaps[1].data[0]).toBe(2); }); });