Skip to content

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
masterfrom
advanced-blend-modes-1318
Open

Advanced blend modes on both GPU backends, and three renderer bugs found on the way (#1318)#1604
obiot wants to merge 1 commit into
masterfrom
advanced-blend-modes-1318

Conversation

@obiot

@obiot obiot commented Aug 26, 2026

Copy link
Copy Markdown
Member

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.

darken and lighten join them: fixed-function MIN/MAX compute min(src, dst) and nothing else, so there was nowhere to put 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.

How

Neither GPU backend can read the destination in a fragment shader — verified directly, not assumed: no WEBGL_blend_equation_advanced_coherent, no EXT_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-language BlendEffect (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.js is 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 and video.AUTO would 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

  • Writing a shader uniform corrupted any pending batch. GLShader.setUniform binds its program to write the value, but GLShader gets 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 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 codeeffect.setUniform() or setTime() 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

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. webgl_available.spec.js was 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

  • Two independent oracles: the Canvas renderer (the browser's own implementation, sharing nothing with this code) and a CPU implementation of the W3C formulas
  • Conformance per mode over two colour pairs, opaque and translucent, plus the color-dodge/color-burn corner cases and soft-light's sqrt branch
  • Adversarial: mode A→B→A, advanced interleaved with fixed-function by pixel, overlapping same-mode draws, save/restore, last-draw-of-frame, setMask, clipRect, ShaderEffect in both the fast path and the chain, camera post-effect nesting, multi-texture scenes
  • WebGPU ordering laws pinned device-free (webgpu_advanced_blend_flow.spec.js), since headless CI has no adapter
  • Every mutation in the plan's table breaks its named test — including a screen_uv mutation that previously survived 36 of 37 tests
  • All 48 examples × 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.

Note for reviewers

The ### Changed entry is real: a game already setting one of these modes rendered unblended and will now look different, and code branching on setBlendMode's return value takes a new path. drawMesh is 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

Copilot AI lite review requested due to automatic review settings August 26, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot force-pushed the advanced-blend-modes-1318 branch from 67fccf0 to f272d91 Compare August 26, 2026 07:58
Copilot AI review requested due to automatic review settings August 26, 2026 07:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…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
obiot force-pushed the advanced-blend-modes-1318 branch from f272d91 to 1f80388 Compare August 26, 2026 08:30
Copilot AI review requested due to automatic review settings August 26, 2026 08:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Advanced blend modes are unsupported on BOTH GPU backends (WebGL 2 and WebGPU)

2 participants