diorama dirty flag

This commit is contained in:
David Allemang
2026-07-06 13:19:25 -04:00
parent 35adf039a6
commit 7f946723ce
3 changed files with 84 additions and 28 deletions

View File

@@ -105,13 +105,14 @@ export class Engine {
this.dioramas = [];
this.frameMeshes = new Map();
this.renderRequested = false;
this.observedFrames = new Set();
this.updateRequested = false;
window.addEventListener('resize', () => this.requestRender());
this.observedFrames = new Set();
this.dirtyFrames = new Set(); // Tracks specific timelines that need mesh updates
// to make sure offscreen dioramas are rendered as soon as they become visible
window.addEventListener('resize', () => this.requestRender());
window.addEventListener('scroll', () => this.requestRender());
}
@@ -137,26 +138,41 @@ export class Engine {
addDiorama(diorama) {
this.dioramas.push(diorama);
diorama.dirty = true;
if (!this.observedFrames.has(diorama.frame)) {
this.observedFrames.add(diorama.frame);
diorama.frame.subscribe(() => this.requestUpdate());
this.dirtyFrames.add(diorama.frame); // Force initial build
diorama.frame.subscribe(() => {
this.dirtyFrames.add(diorama.frame);
// Mark any diorama observing this shared timeline as dirty
for (const d of this.dioramas) {
if (d.frame === diorama.frame) d.dirty = true;
}
this.requestUpdate();
});
} else {
this.requestRender();
}
}
async updateAll() {
const uniqueFrames = new Set(this.dioramas.map(d => d.frame));
if (uniqueFrames.size === 0) return;
if (this.dirtyFrames.size === 0) return;
// Isolate only the frames that mutated
const framesToUpdate = Array.from(this.dirtyFrames);
this.dirtyFrames.clear();
const uniqueBlockIds = new Set();
for (const frame of uniqueFrames) {
for (const frame of framesToUpdate) {
for (const block of frame.blocks.values()) uniqueBlockIds.add(block.id);
}
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
const uniqueModelIds = new Set();
const parsedFrames = new Map();
for (const frame of uniqueFrames) {
const uniqueModelIds = new Set();
for (const frame of framesToUpdate) {
const parsedBlocks = [];
for (const block of frame.blocks.values()) {
const state = await this.cache.get(block.id, 'blockstates');
@@ -170,8 +186,7 @@ export class Engine {
const textureTasks = [];
const framePools = new Map();
for (const frame of uniqueFrames) {
for (const frame of framesToUpdate) {
const instancePool = new Map();
for (const {block, parts} of parsedFrames.get(frame)) {
for (const part of parts) {
@@ -211,7 +226,16 @@ export class Engine {
await Promise.all(textureTasks);
this.updateAtlasTexture();
for (const frame of uniqueFrames) {
for (const frame of framesToUpdate) {
// Memory Cleanup: Free old buffers from the GPU before creating new ones
const oldMeshes = this.frameMeshes.get(frame);
if (oldMeshes) {
for (const mesh of oldMeshes) {
this.gl.deleteVertexArray(mesh.vao);
for (const buf of mesh.buffers) this.gl.deleteBuffer(buf);
}
}
const newMeshes = [];
for (const pool of framePools.get(frame).values()) {
const blockModel = await this.cache.get(pool.partDef.model, 'models');
@@ -226,6 +250,11 @@ export class Engine {
this.frameMeshes.set(frame, newMeshes);
}
// Guarantee that dioramas relying on these newly built meshes are flagged for drawing
for (const d of this.dioramas) {
if (framesToUpdate.includes(d.frame)) d.dirty = true;
}
this.requestRender();
}
@@ -242,8 +271,11 @@ export class Engine {
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const buffers = []; // Track buffers for GC
const bindGeomAttr = (loc, data, size) => {
const buffer = gl.createBuffer();
buffers.push(buffer);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
@@ -257,10 +289,12 @@ export class Engine {
bindGeomAttr(4, geometry.shades, 1);
const ebo = gl.createBuffer();
buffers.push(ebo);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
const matrixBuffer = gl.createBuffer();
buffers.push(matrixBuffer);
gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);
gl.bufferData(gl.ARRAY_BUFFER, matrices, gl.STATIC_DRAW);
@@ -272,6 +306,7 @@ export class Engine {
}
const colorBuffer = gl.createBuffer();
buffers.push(colorBuffer);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.enableVertexAttribArray(9);
@@ -279,21 +314,20 @@ export class Engine {
gl.vertexAttribDivisor(9, 1);
gl.bindVertexArray(null);
return {vao, indexCount: geometry.indices.length, instanceCount};
return {vao, buffers, indexCount: geometry.indices.length, instanceCount};
}
render() {
const gl = this.gl;
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
gl.uniform1i(this.uniforms.texture, 0);
let setupProgram = false;
for (const diorama of this.dioramas) {
const rect = diorama.canvas.getBoundingClientRect();
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) continue;
// Visibility Culling
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) {
continue;
}
const dpr = window.devicePixelRatio || 1;
const targetW = Math.floor(rect.width * dpr);
@@ -302,11 +336,23 @@ export class Engine {
if (diorama.canvas.width !== targetW || diorama.canvas.height !== targetH) {
diorama.canvas.width = targetW;
diorama.canvas.height = targetH;
diorama.dirty = true;
}
// Lazy Render: Only draw if the diorama actually changed
if (!diorama.dirty) continue;
if (!setupProgram) {
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
gl.uniform1i(this.uniforms.texture, 0);
setupProgram = true;
}
if (this.canvas.width < targetW || this.canvas.height < targetH) {
this.canvas.width = targetW;
this.canvas.height = targetH;
this.canvas.width = Math.max(this.canvas.width, targetW);
this.canvas.height = Math.max(this.canvas.height, targetH);
}
gl.viewport(0, 0, targetW, targetH);
@@ -333,6 +379,9 @@ export class Engine {
0, 0, targetW, targetH,
0, 0, targetW, targetH
);
// Clean state!
diorama.dirty = false;
}
}
}