import test from "node:test"; import assert from "node:assert/strict"; import { Renderer } from "../renderer.js"; function fixture(t, { uploadCost = 1, allocationCost = 0 } = {}) { const clock = { now: 0 }; t.mock.method(performance, "now", () => clock.now); let serial = 0, bound; const gl = { ARRAY_BUFFER: 1, DYNAMIC_DRAW: 2, STATIC_DRAW: 3, FLOAT: 4, buffers: new Map(), uploads: [], allocations: [], deletedBuffers: [], deletedVaos: [], createBuffer() { const buffer = { id: ++serial }; this.buffers.set(buffer, new Uint8Array()); return buffer; }, createVertexArray() { return { id: ++serial }; }, bindVertexArray() {}, bindBuffer(_target, buffer) { bound = buffer; }, enableVertexAttribArray() {}, vertexAttribPointer() {}, bufferData(_target, data, usage) { const bytes = typeof data === "number" ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice(); this.buffers.set(bound, bytes); this.allocations.push({ buffer: bound, byteLength: bytes.byteLength, usage }); if (typeof data === "number") clock.now += allocationCost; }, bufferSubData(_target, offset, data) { const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); assert.ok(offset + bytes.byteLength <= this.buffers.get(bound).byteLength); this.buffers.get(bound).set(bytes, offset); this.uploads.push({ buffer: bound, offset, byteLength: bytes.byteLength }); clock.now += uploadCost; }, deleteBuffer(buffer) { this.deletedBuffers.push(buffer); this.buffers.delete(buffer); }, deleteVertexArray(vao) { this.deletedVaos.push(vao); }, }; const renderer = Object.create(Renderer.prototype); Object.assign(renderer, { gl, sections: new Map(), dirty: new Set(["0,0,0"]), meshUpload: null, meshRebuilds: 0, lightViewBounds: { min: [0, 0, 0], max: [31, 31, 31] } }); const old = renderer.mesh(new Float32Array(60).fill(7)); old.transparent = renderer.mesh(new Float32Array(60).fill(8)); old.tag = "retained identity"; const other = renderer.mesh(new Float32Array(60).fill(9)); renderer.sections.set("0,0,0", old); renderer.sections.set("1,0,0", other); const terrain = { epoch: 1, version: 1, ready: new Map(), isCurrent(result) { return result.epoch === this.epoch && result.version === this.version; } }; renderer.terrain = terrain; gl.allocations.length = 0; const result = (opaqueVertices = 3000, transparentVertices = 1000, overrides = {}) => { const vertices = new Float32Array(opaqueVertices * 20), transparent = new Float32Array(transparentVertices * 20); for (let index = 0; index < vertices.length; index++) vertices[index] = index * 0.25; for (let index = 0; index < transparent.length; index++) transparent[index] = -index * 0.125; return { id: "0,0,0", epoch: terrain.epoch, version: terrain.version, vertices, transparent, ...overrides }; }; const queue = (message) => terrain.ready.set(message.id, message); const drain = () => { for (let frame = 0; frame < 200 && (renderer.meshUpload || terrain.ready.size); frame++) renderer.applyTerrainMeshes(0.5); assert.equal(renderer.meshUpload, null); assert.equal(terrain.ready.size, 0); }; return { renderer, terrain, gl, clock, old, other, result, queue, drain }; } test("uploads are capped at 64 KiB and both surfaces publish only after completion", (t) => { const f = fixture(t), message = f.result(); const oldBuffer = f.old.buffer, oldTransparent = f.old.transparent, oldVao = f.old.vao; f.queue(message); f.renderer.applyTerrainMeshes(0.5); assert.equal(f.gl.uploads.length, 1); assert.equal(f.gl.uploads[0].byteLength, 65_536); assert.equal(f.renderer.sections.get(message.id), f.old); assert.equal(f.old.buffer, oldBuffer); assert.equal(f.old.transparent, oldTransparent); assert.deepEqual(f.gl.deletedBuffers, []); assert.ok(f.renderer.dirty.has(message.id)); // Four opaque slices and the first translucent slice still cannot replace // the visible mesh, even though its opaque replacement is already complete. for (let frame = 0; frame < 4; frame++) f.renderer.applyTerrainMeshes(0.5); assert.equal(f.renderer.meshUpload.parts[0].offset, message.vertices.length); assert.ok(f.renderer.meshUpload.parts[1].offset < message.transparent.length); assert.equal(f.old.buffer, oldBuffer); assert.equal(f.old.transparent, oldTransparent); f.drain(); assert.ok(f.gl.uploads.every((upload) => upload.byteLength <= 65_536)); assert.equal(f.renderer.sections.get(message.id), f.old); assert.equal(f.old.tag, "retained identity"); assert.notEqual(f.old.buffer, oldBuffer); assert.notEqual(f.old.vao, oldVao); assert.notEqual(f.old.transparent, oldTransparent); assert.equal(f.old.count, 3000); assert.equal(f.old.transparent.count, 1000); assert.deepEqual(f.gl.buffers.get(f.old.buffer), new Uint8Array(message.vertices.buffer)); assert.deepEqual(f.gl.buffers.get(f.old.transparent.buffer), new Uint8Array(message.transparent.buffer)); assert.ok(f.gl.deletedBuffers.includes(oldBuffer)); assert.ok(f.gl.deletedBuffers.includes(oldTransparent.buffer)); assert.equal(f.renderer.sections.get("1,0,0"), f.other); assert.ok(!f.gl.deletedBuffers.includes(f.other.buffer)); assert.ok(!f.renderer.dirty.has(message.id)); assert.equal(f.renderer.meshRebuilds, 1); }); test("a generation change discards partially uploaded buffers and preserves visible geometry", (t) => { const f = fixture(t), oldBuffer = f.old.buffer, oldTransparent = f.old.transparent; f.queue(f.result(900, 900)); for (let frame = 0; frame < 3; frame++) f.renderer.applyTerrainMeshes(0.5); const staged = f.renderer.meshUpload.parts.map((part) => part.mesh); assert.ok(staged.every(Boolean), "both replacement surfaces were allocated"); assert.equal(f.old.buffer, oldBuffer); f.terrain.version++; f.renderer.applyTerrainMeshes(0.5); assert.equal(f.renderer.meshUpload, null); assert.equal(f.renderer.sections.get("0,0,0"), f.old); assert.equal(f.old.buffer, oldBuffer); assert.equal(f.old.transparent, oldTransparent); assert.ok(f.renderer.dirty.has("0,0,0")); for (const mesh of staged) { assert.ok(f.gl.deletedBuffers.includes(mesh.buffer)); assert.ok(f.gl.deletedVaos.includes(mesh.vao)); } assert.ok(!f.gl.deletedBuffers.includes(oldBuffer)); assert.ok(!f.gl.deletedBuffers.includes(oldTransparent.buffer)); assert.equal(f.renderer.meshRebuilds, 0); f.queue(f.result(3, 0)); f.drain(); assert.equal(f.old.count, 3); assert.equal(f.old.transparent, null); assert.equal(f.renderer.meshRebuilds, 1); }); test("explicit staged-upload disposal is idempotent and owns only replacement resources", (t) => { const f = fixture(t); f.queue(f.result()); f.renderer.applyTerrainMeshes(0.5); const staged = f.renderer.meshUpload.parts[0].mesh; f.renderer.discardMeshUpload(); f.renderer.discardMeshUpload(); assert.equal(f.renderer.meshUpload, null); assert.equal(f.gl.deletedBuffers.filter((buffer) => buffer === staged.buffer).length, 1); assert.equal(f.gl.deletedVaos.filter((vao) => vao === staged.vao).length, 1); assert.ok(f.gl.buffers.has(f.old.buffer)); assert.ok(f.gl.buffers.has(f.old.transparent.buffer)); }); test("empty results remove both old surfaces without allocating empty GPU replacements", (t) => { const f = fixture(t), oldTransparent = f.old.transparent; f.queue(f.result(0, 0)); f.drain(); assert.equal(f.renderer.sections.has("0,0,0"), false); assert.ok(f.gl.deletedBuffers.includes(f.old.buffer)); assert.ok(f.gl.deletedBuffers.includes(oldTransparent.buffer)); assert.equal(f.gl.allocations.length, 0); assert.equal(f.gl.uploads.length, 0); assert.equal(f.renderer.sections.get("1,0,0"), f.other); assert.equal(f.renderer.meshRebuilds, 1); }); test("a new translucent-only section retains an empty opaque handle for draw paths", (t) => { const f = fixture(t), message = f.result(0, 6, { id: "0,1,0" }); f.queue(message); f.drain(); const mesh = f.renderer.sections.get(message.id); assert.ok(mesh?.vao && mesh.buffer); assert.equal(mesh.count, 0); assert.equal(mesh.transparent.count, 6); assert.deepEqual(f.gl.buffers.get(mesh.transparent.buffer), new Uint8Array(message.transparent.buffer)); assert.equal(f.renderer.sections.get("0,0,0"), f.old); }); test("stale or out-of-view results never allocate or replace a mesh", (t) => { const f = fixture(t); f.queue(f.result(3, 0, { version: 0 })); f.renderer.applyTerrainMeshes(0.5); f.queue(f.result(3, 0, { id: "8,0,0" })); f.renderer.applyTerrainMeshes(0.5); assert.equal(f.gl.allocations.length, 0); assert.equal(f.gl.uploads.length, 0); assert.equal(f.renderer.sections.get("0,0,0"), f.old); assert.equal(f.renderer.meshRebuilds, 0); assert.ok(f.renderer.dirty.has("0,0,0")); }); test("the frame budget stops before further upload steps and preserves queued work", (t) => { const f = fixture(t); f.queue(f.result()); f.renderer.applyTerrainMeshes(0); assert.equal(f.gl.allocations.length, 0); assert.equal(f.terrain.ready.size, 1); f.renderer.applyTerrainMeshes(0.5); assert.equal(f.gl.uploads.length, 1); assert.equal(f.renderer.meshUpload.parts[0].offset, 16_384); assert.equal(f.renderer.meshUploadMilliseconds, 1); f.renderer.applyTerrainMeshes(0.5); assert.equal(f.gl.uploads.length, 2); assert.equal(f.renderer.meshUpload.parts[0].offset, 32_768); }); test("an expensive GPU allocation yields before copying vertex data", (t) => { const f = fixture(t, { allocationCost: 3 }); f.queue(f.result()); f.renderer.applyTerrainMeshes(2); assert.ok(f.renderer.meshUpload.parts[0].mesh); assert.equal(f.renderer.meshUpload.parts[0].offset, 0); assert.equal(f.gl.uploads.length, 0); assert.equal(f.renderer.meshUploadMilliseconds, 3); f.renderer.applyTerrainMeshes(0.5); assert.equal(f.gl.uploads.length, 1); assert.equal(f.renderer.sections.get("0,0,0"), f.old); }); test('preview publication keeps the section dirty for corrected lighting and reports visible geometry', t => { const f=fixture(t),published=[]; f.renderer.onMeshPublished=(id,result)=>published.push({id,preview:result.preview}); f.queue(f.result(6,0,{preview:true})); f.drain(); assert.equal(f.old.count,6); assert.equal(f.renderer.dirty.has('0,0,0'),true); assert.deepEqual(published,[{id:'0,0,0',preview:true}]); f.queue(f.result(6,0,{preview:false}));f.drain(); assert.equal(f.renderer.dirty.has('0,0,0'),false); }); test('edit uploads survive unrelated terrain versions but reject newer edits before publication',t=>{ const f=fixture(t),oldBuffer=f.old.buffer; const edits={epoch:9,ticket:1,ready:new Map(),isCurrent(r){return r.epoch===this.epoch&&r.editJob===this.ticket;},cancel(){this.ticket++;}}; f.renderer.edits=edits; edits.ready.set('0,0,0',f.result(3000,0,{epoch:9,editJob:1,preview:true})); f.renderer.applyTerrainMeshes(0.5);const stage=f.renderer.meshUpload; f.terrain.version++; f.renderer.applyTerrainMeshes(0.5); assert.equal(f.renderer.meshUpload,stage,'a distant chunk update must not discard a local edit'); edits.ticket++;f.renderer.applyTerrainMeshes(0.5); assert.equal(f.renderer.meshUpload,null);assert.equal(f.old.buffer,oldBuffer); edits.ready.set('0,0,0',f.result(3,0,{epoch:9,editJob:2,preview:true})); for(let frame=0;frame<10&&(f.renderer.meshUpload||edits.ready.size);frame++)f.renderer.applyTerrainMeshes(0.5); assert.equal(f.old.count,3);assert.equal(f.renderer.dirty.has('0,0,0'),true); f.queue(f.result(3,0));f.drain(); assert.equal(edits.ticket,3,'final lighting cancels any older preview'); assert.equal(f.renderer.dirty.has('0,0,0'),false); });