Repository navigation
fix(gpu): premultiply stencil colors only once - #540
Conversation
main has since decided where the gg fork belongs: only backend/gg/gpu registers the accelerator every fork fix changes, so only that module carries the replace, and the raster and window backends resolve upstream gg again. This branch still had the replace in all three, from before that decision, which is what every conflict was. Resolved main's way, with the pin moved to v0.52.6-figure.5 — the fork build this branch needs, which also premultiplies a stencil fill's alpha once (gogpu/gg#540) — and that PR added to the note above the replace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HdSCXbhBhWBgUiCns9Abhf
kolkov
left a comment
There was a problem hiding this comment.
Checked the color path: the stencil command's color is already premultiplied, and makeCoverUniform multiplied it again. Keeping the [4]float32 through to the uniform fixes it without touching the shader or blend state. This also merges cleanly with the fixes queued for the next release, which touch the same two files.
|
A follow-up from testing the v0.52.6 release: v0.52.6 is out without this one, only because CI never ran on the branch. A push to start CI is all it needs, and I'd like it in v0.52.7. |
0f8d5df to
26abe0d
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
kolkov
left a comment
There was a problem hiding this comment.
Thanks, this is the right fix and the analysis is right. preTessellateFill/preTessellateStroke store premultiplied RGBA in StencilPathCommand.Color, and makeCoverUniform multiplied by alpha a second time, so a translucent stencil fill or stroke came out as alpha²·color + (1−alpha)·B. Opaque colours were unaffected, which is why it depended on the geometry.
I checked both callers of createRenderBuffers: the batch path already carries a premultiplied [4]float32, and RenderPath takes straight alpha and now premultiplies once before handing over. Both correct, and no other caller exists.
The test walks the colour from the paint through tessellation to the cover uniform, for fill and stroke at six alphas, and it fails on main. That is the coverage this needed.
Merging for v0.52.7.
Problem
Translucent fills and strokes rendered through the GPU stencil path are darker and grayer than the CPU output. Opaque colors are correct, as are translucent SDF shapes and convex fills, which makes the difference depend on the geometry being drawn.
GPURenderContext.preTessellateFillstores premultiplied RGBA inStencilPathCommand.Color.buildStencilResourcesBatchthen puts those values into a straight-alphagg.RGBA, andmakeCoverUniformmultiplies RGB by alpha again. Over a background B, the result isalpha² * color + (1 - alpha) * Binstead ofalpha * color + (1 - alpha) * B.This affects concave paths, multiple subpaths in one fill, and strokes whose expanded outline takes the stencil path.
Fix
Keep the command's premultiplied
[4]float32color throughcreateRenderBuffersandmakeCoverUniform, writing it unchanged to the cover shader's uniform. The standaloneStencilRenderer.RenderPathentry point still accepts straight alpha and now premultiplies once before calling the shared buffer builder.No public API change. The shader, blend state, and SDF/convex paths are unchanged.
Minimal example
With
github.com/gogpu/gg/gpuand a hardware backend imported:repro-alpha/main.go renders SDF rectangles, convex fills, concave fills and disjoint subpaths at five alpha values. Each run also renders a CPU reference. Opaque shapes and the first two rows are controls.
In the wild: figure
We found this in figure, a grammar-of-graphics plotting library for Go that renders through gg. Its violin plots use 35% fill opacity. Switching to the GPU made the interiors gray while the opaque outlines and legend colors stayed correct.
repro-alpha-violin/main.go renders the same deterministic gallery example on both tiers:
The CPU reference PNGs are byte-identical before and after the change. Reproduction instructions and measured pixels are on the evidence branch; image files and examples are outside this PR's code diff.
Tests
TestStencilCoverPreservesPremultipliedColorfollows fill and stroke paint through tessellation to the cover uniform at alpha 0, 0.125, 0.35, 0.5, 0.75 and 1. It uses different fill and stroke colors and needs no GPU. Reintroducing the second multiplication makes all eight translucent cases fail.TestMakeCoverUniformnow checks the explicit premultiplied input contract.go test -race ./...,go build ./..., andgolangci-lint run --timeout=5mpass on the isolated PR branch (Windows amd64, Go 1.25.3).