// Header-based allocation bounds only: not a raster decoder or measured GPU memory. import assert from 'node:assert/strict' export const rasterLimits = Object.freeze({ maxRasterBytes: 1048576, maxTextureDimension: 512, maxDecodedTextureBytes: 4 * 1048576 }) export function rasterDimensions(bytes, mimeType) { let width, height if (mimeType === 'image/png') { assert.ok(bytes.length >= 33 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])), 'invalid PNG signature/header') assert.equal(bytes.readUInt32BE(8), 13, 'invalid PNG IHDR length') assert.equal(bytes.toString('ascii', 12, 16), 'IHDR', 'missing PNG IHDR') width = bytes.readUInt32BE(16); height = bytes.readUInt32BE(20) } else { assert.equal(mimeType, 'image/jpeg', 'unsupported raster MIME type') assert.ok(bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216, 'invalid JPEG signature') let offset = 2 while (offset < bytes.length) { assert.equal(bytes[offset++], 255, 'invalid JPEG marker') while (bytes[offset] === 255) offset++ const marker = bytes[offset++] assert.ok(marker !== undefined && marker !== 0 && marker !== 216, 'invalid JPEG marker code') if (marker === 217 || marker === 218) break // SOF must precede EOI/SOS. if (marker === 1 || (marker >= 208 && marker <= 215)) continue assert.ok(offset + 2 <= bytes.length, 'truncated JPEG segment length') const length = bytes.readUInt16BE(offset) assert.ok(length >= 2 && offset + length <= bytes.length, 'truncated JPEG segment') if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) { assert.ok(length >= 8, 'truncated JPEG SOF') height = bytes.readUInt16BE(offset + 3); width = bytes.readUInt16BE(offset + 5) break } offset += length } assert.ok(width !== undefined, 'missing JPEG SOF dimensions') } assert.ok(Number.isInteger(width) && width > 0 && Number.isInteger(height) && height > 0, 'invalid raster dimensions') return { width, height } } export function rasterAllocation(bytes, mimeType, limits = rasterLimits) { const { width, height } = rasterDimensions(bytes, mimeType) assert.ok(Math.max(width, height) <= limits.maxTextureDimension, 'raster dimension budget exceeded') const rgbaBytes = width * height * 4 // Conservative RGBA8 full mip-chain estimate, independent of sampler's minFilter. let mipBytes = 0, w = width, h = height for (;;) { mipBytes += w * h * 4 if (w === 1 && h === 1) break w = Math.max(1, Math.floor(w / 2)); h = Math.max(1, Math.floor(h / 2)) } return { width, height, rgbaBytes, mipBytes } } export function tileRasterBudget(images, textures, limits = rasterLimits) { const allocations = images.map(({ bytes, mimeType }) => rasterAllocation(bytes, mimeType, limits)) const rasterBytes = images.reduce((sum, image) => sum + image.bytes.length, 0) const rgbaBytes = allocations.reduce((sum, image) => sum + image.rgbaBytes, 0) // One allocation per glTF texture, even if multiple samplers share an image. const decodedTextureBytes = textures.reduce((sum, texture) => { assert.ok(Number.isInteger(texture.source) && allocations[texture.source], 'invalid texture image index') return sum + allocations[texture.source].mipBytes }, 0) assert.ok(rasterBytes <= limits.maxRasterBytes, 'embedded raster budget exceeded') assert.ok(decodedTextureBytes <= limits.maxDecodedTextureBytes, 'decoded texture budget exceeded') return { rasterBytes, rgbaBytes, decodedTextureBytes, maxTextureDimension: allocations.reduce((max, image) => Math.max(max, image.width, image.height), 0) } }