Skip to content

Reuse OpenGL state across PBR draws - #48

Open
relh wants to merge 2 commits into
treeform:masterfrom
relh:perf/pbr-state-caching
Open

Reuse OpenGL state across PBR draws#48
relh wants to merge 2 commits into
treeform:masterfrom
relh:perf/pbr-state-caching

Conversation

@relh

@relh relh commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Depends on #47.

What changed

beginPass and endPass let callers reuse OpenGL state across several PBR draws. Blended primitives are held until the pass ends so they can be sorted together. Callers that do not use the new API keep the existing behavior.

This is optional for engines that issue many separate draws; #47 stands on its own.

Checks

Upstream CI passes on every backend.

relh added 2 commits July 2, 2026 22:10
renderPbrPrimitive re-uploaded every uniform, rebound all texture units,
and re-toggled blend/depth/cull for every primitive, then reset that
state after each draw - ~55 GL calls per primitive. A glTF model with
many primitives (the common case) pays this per primitive even when the
frame constants and material are unchanged.

This caches within one ctx.draw:
- Uniform values are per-program GL state, so a PbrPassValues shadow
  dirty-checks the frame-level block (view/proj/lightSpace, lighting,
  fog, camera, tint, debugView, env) and uploads only on change.
- The seven sampler-unit uniforms move to setupPbr (set once).
- Material uniforms upload only when the material or its version changes;
  texture binds key on the actual GL id (so a late texture upload is
  still caught) with an epoch guard against ids recycled by
  glDeleteTextures.
- Blend/depth-mask/cull/front-face go through an enable-state shadow.

The binding/enable caches reset at the start of each ctx.draw (foreign
GL between draws may have changed them) and after the library's own
shadow/skybox passes; the uniform value cache is per-program and
persists. Submission order, per-draw back-to-front blended flush, and
post-draw GL state are unchanged, so output is identical. No API change.

Measured on a dense scene (~1300 primitives, macOS GL): per-primitive
uniform+bind cost drops from the bulk of renderPbrPrimitive's self time
to noise; ~5 GL calls for an opaque same-material primitive vs ~55.
Builds on the within-draw caching. Engines that call ctx.draw(node)
many times per frame (one draw per scene object) reset the binding and
enable caches at every draw boundary, so the caching only helps inside
a single model. beginPass/endPass let such an engine declare that it
owns the GL program, texture units, and enables across a run of draws:

- The binding/enable caches persist across draws within the pass, so a
  field of same-material objects switches material state once, not once
  per object.
- Deferred blended primitives from every draw in the pass flush together
  at endPass, globally sorted back-to-front (per-draw flushing can only
  sort within one draw - a latent transparency-ordering fix).
- invalidateGlState lets the engine interleave its own GL (other shader
  programs) mid-pass; invalidateUniformCache covers direct pbrShader
  uniform writes.

Consumers that never call beginPass are unchanged: ctx.draw wraps itself
in an implicit per-draw scope with identical behavior to the caching
commit. Nesting is refcounted; only the outer pair has effect.

Measured on a dense scene (~1300 draws/frame, macOS GL): the doodad
submit path drops ~19% CPU with the pass held across the object loop.
@relh relh changed the title Opt-in beginPass/endPass to batch PBR state across draws (stacked on #47) Reuse OpenGL state across PBR draws Jul 22, 2026
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.

1 participant