From ec14a62f7fd634fd7f523e0c4ee04cc15c113951 Mon Sep 17 00:00:00 2001 From: Onder Ilke Sever Date: Thu, 23 Apr 2026 08:50:20 +0200 Subject: [PATCH 1/2] fix: fire setDirty when pager fetch pipeline drains (closes #298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a static camera is combined with on-demand rendering, the onDirty chain breaks during paged LOD loading because chunk fetches complete asynchronously — outside any render frame — and nothing signals the renderer that new data is ready to process. Fix: call setDirty() via a new onDirty callback on SplatPager when processFetched() fully empties the fetched queue. This fires after every individual chunk lands, giving progressive rendering. The consumeLodTreeUpdates path is intentionally unchanged; SparkRenderer already calls setDirty() there via the existing lodDirty flow. Co-Authored-By: Claude Sonnet 4.6 --- src/SparkRenderer.ts | 1 + src/SplatPager.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/SparkRenderer.ts b/src/SparkRenderer.ts index 83a96028..a54ea942 100644 --- a/src/SparkRenderer.ts +++ b/src/SparkRenderer.ts @@ -1233,6 +1233,7 @@ export class SparkRenderer extends THREE.Mesh { extSplats: this.pagedExtSplats, maxSplats: this.maxPagedSplats, numFetchers: this.numLodFetchers, + onDirty: () => this.setDirty(), }); const { lodId } = (await worker.call("newLodTree", { diff --git a/src/SplatPager.ts b/src/SplatPager.ts index 58af2f6d..132bf7ef 100644 --- a/src/SplatPager.ts +++ b/src/SplatPager.ts @@ -462,6 +462,7 @@ export interface SplatPagerOptions { * @default 3 */ numFetchers?: number; + onDirty?: () => void; } export class SplatPager { @@ -478,6 +479,7 @@ export class SplatPager { autoDrive: boolean; numFetchers: number; fetchPause = 0; + onDirty?: () => void; splatsChunkToPage: Map< PagedSplats, @@ -573,6 +575,7 @@ export class SplatPager { this.autoDrive = options.autoDrive ?? true; this.numFetchers = options.numFetchers ?? 3; + this.onDirty = options.onDirty; this.splatsChunkToPage = new Map(); this.pageToSplatsChunk = new Array(this.maxPages); @@ -1336,11 +1339,14 @@ export class SplatPager { private processFetched() { const now = performance.now(); + let processed = false; while (true) { const fetched = this.fetched.shift(); if (!fetched) { + if (processed) this.onDirty?.(); break; } + processed = true; const { splats, chunk, data } = fetched; let page = this.allocatePage(); From 00782de7a540deebcc3c3a4ef75042d3ce3c224d Mon Sep 17 00:00:00 2001 From: Onder Ilke Sever Date: Tue, 14 Jul 2026 15:08:50 +0200 Subject: [PATCH 2/2] refactor: rename pager onDirty callback to onLodTreeUpdate per review The SplatPager itself doesn't become "dirty"; the callback fires when the LOD tree gains newly-decoded pages. Renaming to onLodTreeUpdate reflects when the signal is given. SparkRenderer.onDirty is left unchanged since it genuinely signals the renderer needs a redraw. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/SparkRenderer.ts | 2 +- src/SplatPager.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/SparkRenderer.ts b/src/SparkRenderer.ts index a54ea942..ffa662dc 100644 --- a/src/SparkRenderer.ts +++ b/src/SparkRenderer.ts @@ -1233,7 +1233,7 @@ export class SparkRenderer extends THREE.Mesh { extSplats: this.pagedExtSplats, maxSplats: this.maxPagedSplats, numFetchers: this.numLodFetchers, - onDirty: () => this.setDirty(), + onLodTreeUpdate: () => this.setDirty(), }); const { lodId } = (await worker.call("newLodTree", { diff --git a/src/SplatPager.ts b/src/SplatPager.ts index 132bf7ef..fede8c90 100644 --- a/src/SplatPager.ts +++ b/src/SplatPager.ts @@ -462,7 +462,11 @@ export interface SplatPagerOptions { * @default 3 */ numFetchers?: number; - onDirty?: () => void; + /** + * Called when the LOD tree gains newly-decoded pages (the fetch queue + * drains), so on-demand renderers can request a redraw. + */ + onLodTreeUpdate?: () => void; } export class SplatPager { @@ -479,7 +483,7 @@ export class SplatPager { autoDrive: boolean; numFetchers: number; fetchPause = 0; - onDirty?: () => void; + onLodTreeUpdate?: () => void; splatsChunkToPage: Map< PagedSplats, @@ -575,7 +579,7 @@ export class SplatPager { this.autoDrive = options.autoDrive ?? true; this.numFetchers = options.numFetchers ?? 3; - this.onDirty = options.onDirty; + this.onLodTreeUpdate = options.onLodTreeUpdate; this.splatsChunkToPage = new Map(); this.pageToSplatsChunk = new Array(this.maxPages); @@ -1343,7 +1347,7 @@ export class SplatPager { while (true) { const fetched = this.fetched.shift(); if (!fetched) { - if (processed) this.onDirty?.(); + if (processed) this.onLodTreeUpdate?.(); break; } processed = true;