Skip to content

fix(svg): DPR-aware rasterization, source-region crop, zero-copy upload#41

Merged
chiefcll merged 1 commit into
mainfrom
svg-resolution-perf
May 26, 2026
Merged

fix(svg): DPR-aware rasterization, source-region crop, zero-copy upload#41
chiefcll merged 1 commit into
mainfrom
svg-resolution-perf

Conversation

@chiefcll
Copy link
Copy Markdown
Contributor

Summary

Three related fixes to SVG loading in src/core/lib/textureSvg.ts:

  • Sharpness on HiDPI / 4K — rasterize at stage.pixelRatio instead of logical (CSS) pixels. Previously the backing canvas was sized to the requested logical w × h, then the renderer upscaled it on a 2×-DPR display, giving a half-resolution result.
  • Correct source-region cropsx/sy/sw/sh now use the 9-arg drawImage(img, sx, sy, sw, sh, 0, 0, w, h) form, matching the source-rect semantics documented on ImageTextureProps. The old path drew at full destination size then called getImageData(sx, sy, sw, sh), which was a destination-region crop (top-left slice), not a sample of the intended source region.
  • Zero-copy GPU upload — when createImageBitmap is available, the rasterized canvas is converted to an ImageBitmap and returned. The existing WebGL uploader in WebGlCtxTexture.ts:182-196 already accepts ImageBitmap, so the previous getImageData() CPU readback and the w·h·4-byte Uint8ClampedArray allocation are eliminated. ImageData is retained as the Chrome <50 fallback (preserves the Chrome 38 floor per BROWSERS.md).

Also sets img.crossOrigin = 'anonymous' for non-base64 SVG sources so cross-origin SVGs don't taint the canvas and trigger SecurityError on the readback path.

Why

  • Memory: for a 512×512 SVG, removes a ~1 MB Uint8ClampedArray allocation per load (4 MB at 2× DPR). Larger SVGs scale linearly.
  • CPU: removes the synchronous getImageData() readback in the common path.
  • Correctness: the previous crop math silently produced wrong output when sw/sh < w/h. No callers currently exercise that path in-tree, but the prop docs advertise source-crop semantics.
  • Resolution: users on 4K TVs (the engine's target environment) get sharp vector graphics by default instead of bilinear-upscaled rasters.

Touched files

  • src/core/lib/textureSvg.tsloadSvg rewritten (now async, accepts pixelRatio, returns ImageBitmap when available).
  • src/core/CoreTextureManager.ts — added a public pixelRatio getter that forwards to stage.pixelRatio (so ImageTexture doesn't need to reach through private stage).
  • src/core/textures/ImageTexture.ts — passes this.txManager.pixelRatio to loadSvg; collapsed the duplicate type === 'svg' / isSvgImage(src) branches.

Backwards compatibility

  • Public API unchanged. loadSvg is module-local (re-exported only inside the engine), and its new pixelRatio parameter has a single caller in this repo.
  • Texture-size semantics change: a caller passing w: 200, h: 200 on a 2× DPR display now gets a 400×400 GPU texture (still drawn at 200×200 logical). This matches how the engine treats other texture sources at HiDPI and is the whole point of the fix — flagging it here in case any consumer was relying on the prior behavior to keep memory low.
  • premultiplyAlpha: false preserved (canvas-2D output is straight alpha).

Test plan

  • pnpm build — clean
  • pnpm test — 193/193 pass
  • pnpm lint on touched files — no new warnings
  • Manual: load an SVG in the examples app on a 2× DPR display, confirm sharpness
  • Manual: load a cross-origin SVG (with permissive CORS headers), confirm it no longer throws SecurityError
  • Manual: use sx/sy/sw/sh on an SVG texture, confirm the cropped output matches the documented source-region semantics
  • Consider a visual-regression snapshot for an SVG texture under examples/tests/ to lock in the new behavior

🤖 Generated with Claude Code

- Rasterize SVGs at stage.pixelRatio so they stay sharp on HiDPI/4K
  displays instead of being upscaled from a logical-pixel raster.
- Use the 9-arg drawImage form for sx/sy/sw/sh, matching the source-region
  crop semantics documented on ImageTextureProps. The previous code drew
  the SVG at the destination size then cropped via getImageData, which
  produced a destination crop (top-left slice) rather than sampling the
  intended source region.
- Return an ImageBitmap when createImageBitmap is available so the GL
  uploader (which already accepts ImageBitmap/HTMLImageElement) skips
  the forced getImageData CPU readback and the w*h*4 byte allocation.
  Falls back to ImageData on Chrome <50.
- Set img.crossOrigin = 'anonymous' for non-base64 SVG sources so cross-
  origin SVGs don't taint the canvas and trigger SecurityError on read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chiefcll chiefcll force-pushed the svg-resolution-perf branch from 3b70317 to 8ce29f0 Compare May 26, 2026 20:19
@chiefcll chiefcll merged commit 9696d9d into main May 26, 2026
0 of 2 checks passed
@chiefcll chiefcll deleted the svg-resolution-perf branch May 26, 2026 20:40
chiefcll added a commit that referenced this pull request May 26, 2026
…xelRatio < 1 (#44)

PR #41 introduced two unintended dimension changes for SVG textures:

1. Multiplying targetW/H by stage.pixelRatio meant any app rendering at
   sub-1 logical pixel ratio (e.g. the examples app at 720p/1080p =
   0.667) downscaled SVG rasters below the requested size — making
   textures *less* sharp than pre-fix, the opposite of the intent.

2. A node that set only srcWidth/srcHeight (no w/h) used to produce a
   texture sized to the source crop, because the old getImageData(sx,
   sy, sw, sh) returned an sw×sh ImageData regardless of canvas size.
   The new code keyed targetW/H off width || naturalWidth, ignoring sw,
   so the texture became natural-size with a stretched crop drawn into
   it. This broke texture-svg.ts test #4 (rocko2 partial crop, expects
   81x218; was getting 181x218).

Fix:
  - Clamp the DPR multiplier to >= 1 so a sub-1 stage pixelRatio never
    downscales. HiDPI upscaling (the original goal) still applies.
  - Fall through to sw/sh when w/h aren't set, restoring the "crop dims
    drive texture dims" contract.

Refreshed the chromium-ci texture-svg-1.png snapshot — all 5 dimension
+ event assertions now pass (181x218, 200x268, 125x25, 81x218, failure).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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