Skip to content

fix(webgl): premultiply straight bitmaps when device ignores createImageBitmap option#52

Merged
chiefcll merged 4 commits into
mainfrom
fix/premultiply-alpha-ghosting
Jun 3, 2026
Merged

fix(webgl): premultiply straight bitmaps when device ignores createImageBitmap option#52
chiefcll merged 4 commits into
mainfrom
fix/premultiply-alpha-ghosting

Conversation

@chiefcll
Copy link
Copy Markdown
Contributor

@chiefcll chiefcll commented Jun 2, 2026

Summary

Fixes edge ghosting on transparent images on older Safari/WebKit devices that silently ignore the createImageBitmap premultiplyAlpha: 'premultiply' option. Those devices return straight (non-premultiplied) bitmaps, but the WebGL upload hardcoded UNPACK_PREMULTIPLY_ALPHA_WEBGL = false, so the straight pixels were treated as premultiplied — producing the characteristic halo/ghosting around transparent edges.

What changed

  • Unified semantics: TextureData.premultiplyAlpha now consistently means "WebGL should premultiply this source on upload." Each loader path (main-thread bitmap, worker bitmap, basic path, HTMLImageElement fallback, SVG bitmap vs SVG ImageData fallback) sets this intent correctly. The WebGL upload trusts that single flag instead of the old isImageBitmap ? false special-case.
  • GL fallback: when the device ignores the option, request a straight ('none') bitmap and let WebGL premultiply on upload instead.
  • Startup probe (detectPremultiplyAlphaHonored): builds a known straight ImageData, runs it through createImageBitmap({premultiplyAlpha:'premultiply'}), uploads with UNPACK_PREMULTIPLY_ALPHA_WEBGL=false, and reads back via a framebuffer to detect whether the option was actually honored. Honored → red reads back ~128; ignored → ~255.
  • premultiplyAlphaHonored config on RendererMainSettingsboolean | 'auto':
    • omitted (undefined) → true: assume honored, no probe (the default; preserves existing behavior with zero overhead)
    • 'auto' → run the startup probe (auto-detect)
    • boolean → force the value and skip the probe (useful for known fleet devices)
  • Example: examples/tests/premultiply-alpha.ts renders transparent rocko.png over white/black panels in a 2×2 grid to visualize correct vs ghosted output, with an on-screen readout of the detected premultiplyHonored.

Reviewer notes

  • Default is a no-op: with the setting omitted, the fix assumes the option is honored and runs no probe, so behavior is unchanged out of the box. Affected fleets opt in with premultiplyAlphaHonored: 'auto' (detect) or a forced false.
  • The probe is cheap (one 1×1 texture upload + framebuffer readback) and fails safe: any error (no WebGL, ImageData unsupported, incomplete framebuffer) resolves to null, leaving the original behavior unchanged. Rendering only differs when the probe (or a forced config value) reports premultiplyHonored === false.
  • WebGL-only fix; the Canvas2D backend is intentionally untouched.
  • The image worker capability flags are injected via string replacement, so the worker code remains ES5-valid.
  • Unit tests cover detectPremultiplyAlphaHonored (honored / ignored / null branches + GL-premultiply-off during the probe). Note CI's visual snapshot runs on chromium, which honors the option — the non-honored upload path is exercised by unit logic, not the visual suite.

🤖 Generated with Claude Code

chiefcll and others added 4 commits June 2, 2026 16:48
…ageBitmap option

Older Safari/WebKit accepts the createImageBitmap `premultiplyAlpha: 'premultiply'`
option but silently ignores it, returning straight (non-premultiplied) alpha. The
WebGL upload path assumed ImageBitmaps were always premultiplied (hardcoding
UNPACK_PREMULTIPLY_ALPHA_WEBGL = false), so straight pixels reached the GPU and
produced edge ghosting on transparent images.

Fix: unify TextureData.premultiplyAlpha to mean "WebGL should premultiply this
source on upload." When a device is known not to honor the option, create the
bitmap straight ('none') and let WebGL premultiply during texImage2D instead.

- Add a startup probe (detectPremultiplyAlphaHonored) that uploads a known
  semi-transparent pixel and reads it back via a framebuffer to verify the
  option is actually honored, not just accepted.
- Add `premultiplyAlphaHonored` config: undefined -> true (assume honored, no
  probe), null -> run the probe, boolean -> force the value.
- Carry the per-source GL-premultiply intent from each loader (main-thread
  bitmap, ImageWorker, SVG ImageData fallback) through getTextureSource to the
  GL upload, which now honors it uniformly for bitmaps.
- Add premultiply-alpha example for visual regression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…efault

Collapse the three-state config (undefined=assume-honored, null=probe,
boolean=force) into two states on a plain optional boolean:

  premultiplyAlphaHonored?: boolean   // omitted = probe; set = force

This removes the confusing undefined-vs-null distinction and the
double-normalization that was spread across Renderer and Stage.

The probe now runs by default (when the option is omitted) instead of being
opt-in. It is cheap — one 1x1 texture upload + readback at startup — and fails
safe: any error resolves to null, which leaves the original behavior unchanged.
On honored devices it resolves true (no change); on ignored devices it enables
the GL-side premultiply fallback. So the fix now self-heals affected devices
out of the box rather than requiring deployers to know their fleet.

Also adds the previously-missing unit test for detectPremultiplyAlphaHonored
(honored/ignored/null branches + GL-premultiply-off assertion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…robe

Change the probe from default-on to opt-in via an explicit 'auto' sentinel,
restoring the conservative default (assume honored, no probe) so existing
behavior is unchanged out of the box:

  premultiplyAlphaHonored?: boolean | 'auto'
    undefined -> true   (default: assume honored, no probe)
    'auto'     -> run the startup probe
    boolean    -> force the value

'auto' is self-documenting where the previous null-vs-undefined sentinel was
not. Renderer normalizes undefined -> true; Stage coerces the same default so
it is safe when constructed directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chiefcll chiefcll merged commit 94f10d7 into main Jun 3, 2026
1 check passed
@chiefcll chiefcll deleted the fix/premultiply-alpha-ghosting branch June 3, 2026 01:50
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.

1 participant