Skip to content

Skip unchanged material uniforms and skin palettes - #54

Merged
treeform merged 1 commit into
treeform:masterfrom
RowDaBoat:skip-unchanged-material-uniforms
Oct 1, 2026
Merged

treeform merged 1 commit into
treeform:masterfrom
RowDaBoat:skip-unchanged-material-uniforms

Conversation

@RowDaBoat

Copy link
Copy Markdown
Contributor

Problem

applyMaterial re-sends every material uniform on each material switch: about 110 glUniform* calls, most of them the ~22 KHR texture transforms (texCoord, offset, scale, rotation), which are usually still the defaults. Models with a distinct material per primitive switch on every draw. Polyworld's CharGen heroes are an example: one tinted material per body part, 182 different material contents across six heroes. renderPbrPrimitive also re-uploads the full joint palette for every primitive, even when consecutive primitives belong to the same rig.

On native GL this is cheap. In the browser it isn't: Chrome validates and translates each WebGL call in its GPU process. Profiling the Archers Warriors Mages web build (Chrome 153, ANGLE Metal, M1 Max) showed ~10,900 WebGL calls per frame for ~90 draws, about 8,700 of them scalar uniforms from applyMaterial. Decoding them cost the GPU process ~25 ms per frame.

Change

  • Material uniforms: they go through ctx.setUniform1i/1f/2f/3f/4f, which keep a per-location shadow of the values pbrShader holds (ctx.uniformShadow). The real glUniform* is called only when the value changed. Values are compared as raw bits, so NaNs and -0.0 behave correctly.
  • Joint palette: renderPbrPrimitive skips jointMatrices when it equals the palette last uploaded (ctx.lastJointMatrices).
  • Invalidation: both caches reset where the uniform cache already resets: invalidateUniformCache (also called on IBL variant switches) and both places that reset passValues. This follows the existing syncPassValue contract: uniform values are per-program GL state and survive foreign program binds.
  • Large locations: locations at or above MaxShadowedUniforms (4096) always upload, so the shadow never grows unbounded.

Nothing else changes: the lastMaterial early return, texture binds and per-pass uniforms are untouched.

Results

Same AWM web build and scene:

Before After
WebGL calls per frame ~10,900 ~1,900
GPU-process WebGL command time per frame ~25 ms ~4 ms
p95 frame time (30 fps-capped run) 57 ms 34 ms

The machine was in Low Power Mode, where Chrome caps pages at 30 fps, so these frame times don't show the uncapped frame rate.

Testing

  • nim r tests/tests.nim passes.
  • tests/test_pbr_pass.nim, which checks images and that the uniform cache never serves stale values, was not run: the glTF-Sample-Assets checkout wasn't available. Please run it, or the reference-image suite, before merging.
  • The AWM web build was rendered with the change, but its output wasn't compared pixel by pixel against a build without it.

🤖 Generated with Claude Code

applyMaterial re-sent ~110 uniforms on every material switch, most of
them default texture transforms that already match what pbrShader
holds. Parts of one rig also re-uploaded the same joint palette for
every primitive. Material uniforms now go through a per-location
shadow of the values pbrShader holds, and the joint palette uploads
only when it changed. Both reset with the existing uniform cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@treeform
treeform merged commit adbf72a into treeform:master Oct 1, 2026
9 checks 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