Skip to content

onDirty chain breaks during paged LOD loading when camera is static #298

@onderilkesever

Description

@onderilkesever

Description

When using SparkRenderer with paged LOD splats in an on-demand rendering architecture (render only when explicitly requested, no continuous render loop), the onDirty callback chain has gaps that cause splats to not fully load.

Reproduction

  1. Create a SparkRenderer with preUpdate: true and an onDirty callback that schedules a new render frame
  2. Add a SplatMesh with { paged: new PagedSplats({ rootUrl }), lod: true }
  3. Trigger a single render (the camera is static after this)
  4. Observe: the splat sometimes loads fully, sometimes stops partway through

Workaround

We currently poll invalidateScene() every 200ms while pager.fetchers, pager.fetched, pager.newUploads, or pager.lodTreeUpdates are non-empty.

Potential Root Cause Analysis (AI-assisted, via Claude)

We used Claude to trace through the compiled SparkRenderer source and it identified the following potential explanation. This is AI-assisted analysis — it may not be fully accurate, but we're sharing it in case it's helpful.

setDirty() appears to be called in only 3 places inside SparkRenderer:

  1. After generate() in updateInternal — but only when doUpdate = true. When the camera is static and the version hasn't changed, doUpdate = false and this is skipped.
  2. After sort completes in driveSort — but driveSort returns immediately when sortDirty = false, which happens when doUpdate = false (since sortDirty = true is only set inside the doUpdate branch).
  3. After updateLodInstances in driveLod — but only when tryExclusive succeeds (LOD worker is free) AND lodDirty = true.

Claude's hypothesis is that the chain breaks when all three conditions fail simultaneously:

  • Camera is static → doUpdate = false → no setDirty from (1) or (2)
  • LOD worker is busy processing a previous batch → tryExclusive returns null → no setDirty from (3)

Meanwhile, chunks are still being downloaded by the pager's fetchers in the background. When they complete, nothing calls setDirty(), so no new render is scheduled to process them.

The chain eventually resumes when the LOD worker finishes its current batch (calling setDirty at the end of tryExclusive), but the gap can be significant — especially during initial load when initLodTree involves network I/O for the .rad metadata.

Additionally, consumeLodTreeUpdates() may return empty between LOD traversals even though chunks are still in flight. If lodDirty is false at that point, updateLodInstances is skipped and setDirty is never called.

Suggested Fix (AI-assisted, via Claude)

Consider calling setDirty() when paged chunk fetches complete (e.g., in processFetched or after driveFetchers starts new downloads), so that on-demand renderers are notified that new data is available for processing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions