Skip to content

perf: reuse clear options object in Renderer.clear#8776

Merged
willeastcott merged 2 commits into
mainfrom
perf/renderer-clear-no-alloc
May 26, 2026
Merged

perf: reuse clear options object in Renderer.clear#8776
willeastcott merged 2 commits into
mainfrom
perf/renderer-clear-no-alloc

Conversation

@willeastcott

@willeastcott willeastcott commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Renderer.clear was allocating a fresh { color, depth, stencil, flags } object — and a fresh [r, g, b, a] color array — on every call. The method runs per camera per layer per frame, so this added up to sustained per-frame GC pressure.

Hoist both to module-scope scratch buffers (matching the _temp* naming used elsewhere in this file) and write field values in place. Behaviour is unchanged: the WebGL, WebGPU, and Null clear implementations only read from the options object, they never store it.

Technical details

// before — fresh object + fresh array on every call
device.clear({
    color: [camera._clearColor.r, camera._clearColor.g, camera._clearColor.b, camera._clearColor.a],
    depth: camera._clearDepth,
    stencil: camera._clearStencil,
    flags: flags
});

// after — module-scope scratch reused across calls
const c = camera._clearColor;
_tempClearColor[0] = c.r;
_tempClearColor[1] = c.g;
_tempClearColor[2] = c.b;
_tempClearColor[3] = c.a;
_tempClearOptions.depth = camera._clearDepth;
_tempClearOptions.stencil = camera._clearStencil;
_tempClearOptions.flags = flags;
device.clear(_tempClearOptions);

Verified that all three clear(options) implementations only read fields and do not retain a reference:

  • `webgl-graphics-device.js#clear` — reads `options.flags`, `options.color`, `options.depth`, `options.stencil`
  • `webgpu-graphics-device.js#clear` — passes options through to `clearRenderer.clear`, which only reads
  • `null-graphics-device.js#clear` — empty

Measured impact

Captured via V8 sampling heap profiler (`node:inspector` `HeapProfiler.startSampling`) on a representative scene rendered against `NullGraphicsDevice`:

Metric Before After Δ
`Renderer.clear` in a tight call loop 48 B/iter ~0 B/iter −100%
Engine-wide allocation sampled per frame on the typical-scene fixture 201 B/frame 164 B/frame −18%

Public API changes

None. The options shape passed to `GraphicsDevice.clear` is unchanged; only the underlying memory is now reused.

Test plan

  • Existing unit tests pass (`npm test`)
  • Visually verify a basic scene clears correctly on WebGL2 and WebGPU

`Renderer.clear` was allocating a fresh `{ color, depth, stencil, flags }`
object — and a fresh `[r, g, b, a]` color array — on every call. The
method runs per camera per layer per frame, so this added up to a
sustained per-frame allocation.

Hoist both to module-scope scratch buffers and write the field values in
place. Behaviour is unchanged: the WebGL, WebGPU, and Null device `clear`
implementations only read from the options object, they never store it.

Measured impact on a typical-scene allocation profile (1800 frames,
NullGraphicsDevice, V8 sampling heap profiler @ 128 B interval):

  - Renderer.clear attribution: 48 B/iter -> 0 B/iter (in a tight call loop)
  - Engine-wide allocation sampled per frame on the same fixture:
      201 B/frame -> 164 B/frame (-18%)
The scratch variables in this file all use the _temp* prefix
(_tempLightSet, _tempLayerSet, _tempProjMat0..5, _tempSet,
_tempMeshInstances). Match that rather than introducing a new Scratch
suffix.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reduces per-frame GC pressure in the scene renderer by reusing scratch buffers for GraphicsDevice.clear options, avoiding repeated allocations in a hot path (Renderer.clear, invoked per camera/layer/frame).

Changes:

  • Hoists a reusable clear color array to module scope and updates its RGBA values in-place per call.
  • Hoists a reusable clear options object to module scope and updates depth/stencil/flags in-place per call before invoking device.clear(...).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@willeastcott willeastcott merged commit 8e2f56d into main May 26, 2026
9 checks passed
@willeastcott willeastcott deleted the perf/renderer-clear-no-alloc branch May 26, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Relating to load times or frame rate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants