import { createHash } from "node:crypto"; const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); /** Validate a legacy DXT1 DDS and describe every source mip without decoding or resampling it. */ export function inspectNativeWaterDxt1Dds(bytes) { if (!Buffer.isBuffer(bytes)) bytes = Buffer.from(bytes); if (bytes.length < 128 || bytes.toString("ascii", 0, 4) !== "DDS ") { throw new Error("Native water frame is not a complete DDS file."); } const headerSize = bytes.readUInt32LE(4); const height = bytes.readUInt32LE(12); const width = bytes.readUInt32LE(16); const pixelFormatSize = bytes.readUInt32LE(76); const formatFourCC = bytes.toString("ascii", 84, 88); const mipCount = Math.max(1, bytes.readUInt32LE(28)); if (headerSize !== 124 || pixelFormatSize !== 32) { throw new Error(`Unsupported DDS header sizes ${headerSize}/${pixelFormatSize}.`); } if (formatFourCC !== "DXT1") throw new Error(`Unsupported native water frame format ${formatFourCC}.`); if (width < 1 || height < 1 || width % 4 !== 0 || height % 4 !== 0) { throw new Error(`Native water DXT1 dimensions must be positive multiples of four; got ${width}x${height}.`); } const maxMipCount = 1 + Math.floor(Math.log2(Math.max(width, height))); if (mipCount > maxMipCount) throw new Error(`DDS declares ${mipCount} mips for ${width}x${height}.`); let offset = 128; let mipWidth = width; let mipHeight = height; const mips = []; for (let level = 0; level < mipCount; level += 1) { const byteLength = Math.ceil(mipWidth / 4) * Math.ceil(mipHeight / 4) * 8; const end = offset + byteLength; if (end > bytes.length) throw new Error(`DDS mip ${level} is truncated.`); const data = bytes.subarray(offset, end); mips.push({ level, width: mipWidth, height: mipHeight, offset, byteLength, sha256: sha256(data), }); offset = end; mipWidth = Math.max(1, Math.floor(mipWidth / 2)); mipHeight = Math.max(1, Math.floor(mipHeight / 2)); } if (offset !== bytes.length) { throw new Error(`DDS has ${bytes.length - offset} trailing bytes after its declared mip chain.`); } return { width, height, mipCount, formatFourCC, headerBytes: 128, fileBytes: bytes.length, sha256: sha256(bytes), mips, exactMipChainConsumption: offset === bytes.length, }; }