Cache PBR uniform values and GL binds; add beginPass/endPass - #46
Closed
relh wants to merge 2 commits into
Closed
Conversation
relh
force-pushed
the
perf/pbr-state-caching
branch
from
July 3, 2026 01:25
71e27af to
71bd94b
Compare
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
force-pushed
the
perf/pbr-state-caching
branch
from
July 3, 2026 05:12
71bd94b to
0b38331
Compare
Contributor
Author
|
Splitting this into a reviewable general win (within-draw uniform/bind caching, no API change) and a separate opt-in cross-draw pass API. Reopening the caching half as a fresh single-commit PR; the pass API stacks on it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
renderPbrPrimitivere-submits the full GL state for every primitive, every frame: an unconditionalglUseProgram, view/proj/lightSpace uploads, ~15 lighting/fog/camera uniforms, the env-cubemap bind, 6–7 texture-unit binds plus ~20 material uniforms, and blend/depth/cull toggles — then a full state reset after each draw. That's ~55 GL calls per primitive; in a scene with 1000+ primitives it dominates CPU submit time.Change (one commit, rebased on current master)
PbrPassValuesshadow and uploaded only on change. Always on — no API, no behavior change.setupPbr(they never change).(material, materialVersion); texture binds keyed on the actual GL ids (so a texture that streams in later is still caught), with an epoch guard against ids recycled afterglDeleteTextures.beginPass()/endPass()/invalidateGlState()for engines that callctx.draw(node)many times per frame: scopes the bind caches across draws and defers blended primitives from all draws into one globally sorted back-to-front flush (per-draw flushing can't sort blends across trees).invalidateGlState()covers callers that run their own GL mid-pass. Consumers that never callbeginPassget an implicit per-draw pass — identical submission order, per-draw blended flush, and post-draw GL state as today.#44's tint-alpha fade semantics are preserved:
tint.a < 1still defers draws to the blend pass and blends opaque materials with depth writes on; in pass mode each deferred entry replays with the tint captured at defer time, so cross-draw fades sort and blend correctly.Measured (macOS GL, ~1300 primitives/frame)
Native sampling: the per-primitive uniform+bind block drops from the bulk of
renderPbrPrimitive's self time to noise (7 + 16 samples over 10 s); submit-path CPU −19%. Hot path for an opaque same-material primitive: ~5 GL calls vs ~55.Notes for review
tests/tests.nimfails at master with adecodeImagePixieError before this change too (test asset issue);backend_shaders.nimcompiles clean.