fix(render): ceil canvas dimensions so 4K full-screen doesn't leak the wrap bg - #31
Open
winlp4ever wants to merge 1 commit into
Open
fix(render): ceil canvas dimensions so 4K full-screen doesn't leak the wrap bg#31winlp4ever wants to merge 1 commit into
winlp4ever wants to merge 1 commit into
Conversation
…e wrap bg On high-DPI screens — notably 4K full-screen — ResizeObserver can report fractional CSS widths (e.g. `3840.4`). The previous sizing math used `Math.round` for the backing store and the raw fractional value for `canvas.style.width`. At certain widths and DPRs, the browser snaps the canvas's CSS dim down at layout time, leaving the canvas ~0.5 CSS px narrower than the wrap div. The wrap's non-theme-aware background (`#f8fafc`) shows through as a faint vertical strip on the right — invisible in light mode, a visible white line in dark mode during quick zoom-out (when the static surface is cleared+repainted per tier-3 frame, briefly exposing the strip before the next paint covers it). Switches to `Math.ceil` for both backing and CSS dimensions, and stores the ceiled value as `surface.cssWidth`/`cssHeight` so the renderer's viewport math (used in `worldViewport`, `paintBackground`, etc.) sees the same dimensions the canvas is actually displaying. The canvas now ends up ≤ 1 CSS px wider than the wrap; the wrap's `overflow: hidden` clips that sub-pixel overflow to invisibility. Also updates the early-out comparison in `sizeSurface` to use the ceiled values — otherwise a fractional ResizeObserver report that ceils to the same integer as the cached value would re-allocate the backing store every call (flicker on 4K). No behavior change at integer cssW/cssH values (the common case). 351+1 node tests, 61 browser tests, all green.
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.
Problem
On high-DPI displays (notably 4K full-screen), `ResizeObserver` can report fractional CSS widths (e.g. `3840.4`). The previous canvas-sizing math:
```ts
surface.canvas.width = Math.max(1, Math.round(cssW * dpr)) // backing: rounded down
surface.canvas.style.width = `${cssW}px` // CSS: raw fractional
```
…rounded the backing store to integer device pixels but kept the raw fractional value for the CSS dimension. At certain widths/DPRs, the browser snaps that CSS dim down at layout time, leaving the canvas ~0.5 CSS px narrower than the wrap div. The wrap's background (`#f8fafc`, hard-coded in Canvas.tsx, not theme-aware) shows through that thin strip on the right edge.
Slightly resizing the window away from 4K-exact makes `cssW` integer-aligned and the gap disappears — which matches what was reported.
Fix
Use `Math.ceil` (not `Math.round`) consistently for backing store, CSS dim, and the stored `surface.cssWidth`/`cssHeight`:
```ts
const ceilCssW = Math.ceil(cssW)
const ceilCssH = Math.ceil(cssH)
surface.canvas.width = Math.max(1, Math.round(ceilCssW * dpr))
surface.canvas.height = Math.max(1, Math.round(ceilCssH * dpr))
surface.canvas.style.width = `${ceilCssW}px`
surface.canvas.style.height = `${ceilCssH}px`
surface.cssWidth = ceilCssW
surface.cssHeight = ceilCssH
```
This guarantees the canvas is always ≥ wrap width, never narrower. Sub-pixel overflow (< 1 CSS px) is clipped invisibly by the wrap's existing `overflow: hidden`.
Side fix
Updates the early-out check in `sizeSurface` to compare against the ceiled values too. Without that, a fractional ResizeObserver report that ceils to the same integer as the cached value would re-allocate the canvas backing store on every call — visible flicker on 4K.
Why the renderer's viewport math also gets the ceiled value
`surface.cssWidth` flows into `worldViewport`, `paintBackground`, and the cache-source-rect helpers. Storing the raw fractional value while displaying the ceiled value would mean the renderer thinks the viewport is one size while the canvas is actually displaying another — sub-pixel discrepancy at the right edge. Storing the ceiled value keeps both halves consistent.
No regressions for the common case
Tests run at integer `cssW` / `cssH` values. `Math.ceil(N)` of an integer is the same integer, so the existing test fixtures behave identically.
Test plan