Skip to content

perf: dirty quad buffer with surgical bufferSubData uploads - #861

Merged
wouterlucas merged 2 commits into
lightning-js:mainfrom
wouterlucas:perf/dirty-quad-buffer
Aug 7, 2026
Merged

perf: dirty quad buffer with surgical bufferSubData uploads#861
wouterlucas merged 2 commits into
lightning-js:mainfrom
wouterlucas:perf/dirty-quad-buffer

Conversation

@wouterlucas

Copy link
Copy Markdown
Contributor

Description

Optimizes the per-frame quad upload in the WebGL renderer so that only the quads that actually changed are re-uploaded to the GPU, instead of uploading the entire quad buffer on every frame.

How it works

  • Every main-scene node now owns a permanent slot (quadBufferIndex) in the quad buffer, assigned contiguously during the addQuad pass.
  • When visual data changes (transforms, colors, alpha, texture), the node is marked dirty (isQuadDirty) in CoreNode.update().
  • In render(), the renderer chooses between:
    • Surgical path: gl.bufferSubData per dirty node, copying each 20-float quad into a preallocated scratch buffer (no per-frame allocation).
    • Full upload: a single gl.bufferData when the render list changes structurally, the buffer grows, or more than 40% of the render list is dirty (N bufferSubData calls are more expensive than one bulk upload at that point).
  • RTT renders use a dedicated CPU buffer and are unaffected by the main scene's slot bookkeeping; after an RTT pass the main buffer is force re-uploaded.
  • Stage.requestRenderListUpdate() notifies the renderer (invalidateQuadBuffer()) on structural changes so slots are reset and everything is re-uploaded.

Changes

  • src/core/CoreNode.tsquadBufferIndex / isQuadDirty bookkeeping + dirty marking
  • src/core/Stage.ts — expose render list; call renderer.invalidateQuadBuffer() on rebuilds
  • src/core/platforms/GlContextWrapper.ts, WebGlContextWrapper.tsarrayBufferSubData
  • src/core/renderers/CoreRenderer.ts — optional invalidateQuadBuffer() hook
  • src/core/renderers/webgl/WebGlRenderer.ts — dirty-ratio upload decision, surgical bufferSubData, dedicated RTT buffer
  • Tests: CoreNode.test.ts, Stage.renderListUpdate.test.ts, WebGlRenderer.dirtyQuadBuffer.test.ts
  • Visual regression: examples/tests/dirty-quad-buffer.ts + certified snapshots (covers move, color, alpha, RTT child move, add, remove)

Verification

  • pnpm build passes
  • pnpm vitest run — 509/509 tests pass
  • pnpm lint clean

jfboeve
jfboeve previously approved these changes Aug 7, 2026
@wouterlucas
wouterlucas dismissed jfboeve’s stale review August 7, 2026 10:14

The merge-base changed after approval.

jfboeve
jfboeve previously approved these changes Aug 7, 2026
@wouterlucas
wouterlucas dismissed jfboeve’s stale review August 7, 2026 10:19

The merge-base changed after approval.

jfboeve
jfboeve previously approved these changes Aug 7, 2026
@wouterlucas
wouterlucas dismissed jfboeve’s stale review August 7, 2026 10:39

The merge-base changed after approval.

@wouterlucas
wouterlucas merged commit 50c5a4f into lightning-js:main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants