Advanced blend modes on both GPU backends, and three renderer bugs found on the way (#1318) - #1604
Open
obiot wants to merge 1 commit into
Open
Advanced blend modes on both GPU backends, and three renderer bugs found on the way (#1318)#1604obiot wants to merge 1 commit into
obiot wants to merge 1 commit into
Conversation
obiot
force-pushed
the
advanced-blend-modes-1318
branch
from
August 26, 2026 07:58
67fccf0 to
f272d91
Compare
…und on the way (#1318) The six CSS blend modes that fixed-function blending cannot express — overlay, hard-light, color-dodge, color-burn, soft-light, difference — worked only on the Canvas renderer and silently fell back to "normal" on WebGL 2 and WebGPU. Setting `sprite.blendMode = "overlay"` gave no error and no effect. All thirteen modes the engine names now work on all three renderers, so the Canvas fallback is no longer the most capable backend for blending. Neither GPU backend can read the destination in a fragment shader (verified: no WEBGL_blend_equation_advanced_coherent, no framebuffer fetch, no pixel local storage; gpuweb#394 still open), so each such draw captures the destination, renders to an offscreen target, and composites through a dual-language BlendEffect carrying both a GLSL and a WGSL body. Hooked at `setBatcher`, the one point every draw entry point passes through, so it covers sprites, text, image layers, particles, Tiled layers and shape fills identically — per draw, against the live framebuffer, the way Canvas and the fixed-function modes already behave. `darken` and `lighten` join them. Fixed-function MIN/MAX compute `min(src, dst)` and nothing else, leaving nowhere for the `(1 - srcAlpha) * dst` term source-over contributes after the blend: at 60% opacity darken was 84/255 off the W3C result, and a white `lighten` glow over a light backdrop rendered completely invisible. Measurement also showed multiply, screen and exclusion to be exact at any alpha, so the "approximate for a translucent source" comments they carried were simply wrong and are gone. One shared registry, `video/blendmodes.js`, now defines every mode once. The two GPU backends each carried their own copy — a switch of blendEquation/blendFunc calls on one side, a GPUBlendState table on the other — so a mode corrected on one could silently disagree with the other and `video.AUTO` would render the same scene differently depending on what it picked. Verified pixel-identical before and after across all thirteen modes. THREE PRE-EXISTING BUGS, none of them about blend modes: - Writing a shader uniform corrupted any batch still pending. `GLShader.setUniform` binds its program to write the value, and linking leaves one bound, but GLShader is constructed with a bare `gl` and cannot reach the renderer's program cache — so the cache named the batcher's program while GL had the effect's. Batchers check that cache before re-issuing useProgram, skipped the rebind as redundant, and drew queued geometry through the wrong program: sprites turned black or vanished, silently, because the attribute layouts overlap. Reachable from ordinary code: `effect.setUniform()` between two draws is enough. The cache now lives on the GL context, the one object both sides hold. - `CanvasRenderTarget.invalidate()` re-entered the batcher dispatch, so refreshing a texture looked like a scene draw and bracketed the invalidation itself. - `Batcher.bind()` used the program cache to decide whether to adopt its own shader, two different questions that only coincided while the cache could lie. The first two were found by looking at a screenshot, not by 6000 tests, because `getWebGLRenderer` caught every construction error and treated it as "this machine has no WebGL" — turning engine breakage into a green run of skips across 47 spec files. It now classifies: a genuinely missing GL stack still skips, anything else fails loudly carrying the original error. Verified against the Canvas renderer, which implements all six natively and shares nothing with this code, as well as against a CPU implementation of the W3C formulas. Every example (48 x 3 backends) compared against master with a master-vs-master control to separate animation noise from real change: the Blend Modes grid is the only thing that differs. 6250 tests / 258 spec files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
obiot
force-pushed
the
advanced-blend-modes-1318
branch
from
August 26, 2026 08:30
f272d91 to
1f80388
Compare
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.
Closes #1318.
The six CSS blend modes fixed-function blending cannot express —
overlay,hard-light,color-dodge,color-burn,soft-light,difference— worked only on Canvas and silently fell back to"normal"on WebGL 2 and WebGPU. All thirteen modes the engine names now work on all three renderers.darkenandlightenjoin them: fixed-functionMIN/MAXcomputemin(src, dst)and nothing else, so there was nowhere to put the(1 - srcAlpha) * dstterm source-over contributes after the blend. At 60% opacitydarkenwas 84/255 off the W3C result, and a whitelightenglow over a light backdrop rendered completely invisible.How
Neither GPU backend can read the destination in a fragment shader — verified directly, not assumed: no
WEBGL_blend_equation_advanced_coherent, noEXT_shader_framebuffer_fetch, no pixel local storage, and gpuweb#394 still open. So each such draw captures the destination, renders to an offscreen target, and composites through a dual-languageBlendEffect(GLSL + WGSL).Hooked at
setBatcher, the one point every draw entry point passes through, so sprites, text, image layers, particles, Tiled layers and shape fills are all covered by the same code — per draw, against the live framebuffer, exactly how Canvas and the fixed-function modes already behave.video/blendmodes.jsis now the single registry. The two GPU backends each carried their own copy of the table, so a mode corrected on one could silently disagree with the other andvideo.AUTOwould render the same scene differently depending on what it picked. Verified pixel-identical across all thirteen modes before and after.Three pre-existing bugs, none about blend modes
GLShader.setUniformbinds its program to write the value, butGLShadergets a baregland cannot reach the renderer's program cache — so the cache named the batcher's program while GL had the effect's. Batchers skipped the rebind as redundant and drew queued geometry through the wrong program: sprites turned black or vanished, silently, because the attribute layouts overlap. Reachable from ordinary code —effect.setUniform()orsetTime()between two draws is enough, with no blend mode involved.CanvasRenderTarget.invalidate()re-entered the batcher dispatch, so refreshing a texture looked like a scene draw.Batcher.bind()used the program cache to decide whether to adopt its own shader — two different questions that coincided only while the cache could lie.Why the suite didn't catch them
getWebGLRenderercaught every construction error and treated it as "this machine has no WebGL", turning engine breakage into a green run of skips across 47 spec files.webgl_available.spec.jswas the backstop but only runs with the full suite, so a subset run lost the signal entirely. It now classifies: a missing GL stack still skips, anything else fails loudly with the original error.Two of the three bugs above were found by looking at a screenshot rather than by 6000 tests.
Verification
color-dodge/color-burncorner cases andsoft-light'ssqrtbranchsave/restore, last-draw-of-frame,setMask,clipRect,ShaderEffectin both the fast path and the chain, camera post-effect nesting, multi-texture sceneswebgpu_advanced_blend_flow.spec.js), since headless CI has no adapterscreen_uvmutation that previously survived 36 of 37 testsmaster, with a master-vs-master control to separate animation noise from real change. The Blend Modes grid is the only thing that differs6250 tests / 258 spec files.
Note for reviewers
The
### Changedentry is real: a game already setting one of these modes rendered unblended and will now look different, and code branching onsetBlendMode's return value takes a new path.drawMeshis the one remaining exclusion — the offscreen's separate depth buffer would break subsequent depth testing — and falls back with a one-time warning rather than silently.🤖 Generated with Claude Code
https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N