Reuse OpenGL state across PBR draws - #48
Open
relh wants to merge 2 commits into
Open
Conversation
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.
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.
Depends on #47.
What changed
beginPassandendPasslet 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.