| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 |
- // 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) }
- }
|