webgl: fix terminal drawn at the wrong scale when the device pixel ratio changes - #6115
Open
seanhaufler wants to merge 1 commit into
Open
webgl: fix terminal drawn at the wrong scale when the device pixel ratio changes#6115seanhaufler wants to merge 1 commit into
seanhaufler wants to merge 1 commit into
Conversation
_setCanvasDevicePixelDimensions resizes the drawing buffer whenever DevicePixelObserver reports a new device pixel content box, but the viewport and the resolution the shaders divide by are only ever set in handleResize, from dimensions.device.canvas. That is fine for the sub-pixel rounding differences the observer exists to correct, but a device pixel ratio change is reported through the same callback, and there the buffer changes by a whole factor while the viewport still covers the old rectangle. The terminal is then drawn at the wrong scale into a corner of the buffer, which the browser scales down into the element. ScreenDprMonitor is meant to see the ratio change first and drive handleDevicePixelRatioChange, but it only listens for a window resize and a resolution media query, and a terminal in a frame whose CSS size is fixed can get neither. Nothing re-checks afterwards, so the terminal stays mis-scaled until something else resizes it. Handle the ratio change here as well. Whichever path arrives first, the second finds the ratio already adopted and does nothing.
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.
What's wrong
WebglRenderer._setCanvasDevicePixelDimensionsresizes the WebGL drawing buffer wheneverDevicePixelObserverreports a new device pixel content box, then only asks for a redraw:The GL viewport and the resolution the shaders divide every vertex by are set in one place only,
GlyphRenderer.handleResize, and fromdimensions.device.canvasrather than from the canvas:That's fine for what the observer is there for, "correcting rounding errors when converting between CSS pixels and device pixels", where the difference is sub-pixel. But the same observer also reports a change in the device pixel ratio itself, and there the difference is a whole factor. The buffer doubles while the viewport still covers the old rectangle and the shaders still map the old resolution, so the terminal gets drawn at half scale into a corner of the buffer, which the browser then scales down into the element. Same rows, same columns, all the text present, just drawn small with blank space beside and below it.
ScreenDprMonitoris supposed to notice the ratio change first and drivehandleDevicePixelRatioChange, which recomputes the dimensions at the new ratio and resizes the renderer properly. It can't be relied on to get there. Its triggers are the window'sresizeevent and a(resolution: <dpr>dppx)media query, and a terminal in a frame whose CSS size is fixed gets no resize when the ratio under it changes: the frame's CSS dimensions don't move, only the ratio underneath them. Nothing re-checks afterwards, so once it's missed the terminal stays mis-scaled until something else resizes it, which in practice means until the page is reloaded.Where I ran into it
I embed terminals in fixed size iframes in an internal dashboard, several kept mounted at once with only the selected one on screen, on Chrome and macOS. The usual trigger is closing the laptop and opening it again. On wake the displays are re-established and the ratio under those frames can change, but the frames themselves never resize, so the media query is the only detector left and a pane that was hidden at the time can miss it. Some proportion of the time a pane then comes back drawn at roughly half size in the corner of its box, and stays that way until the frame is reloaded. The pty reports the same rows and columns throughout and none of the text is missing, only the scale is wrong, which is what pointed at the viewport rather than at sizing.
I haven't caught it in the act with a debugger attached, so the wake from sleep part of that is inference from the symptom rather than something I watched happen. What I can show directly is the mechanism, below: put the renderer into the state a missed ratio change leaves it in and the mis-scaled terminal appears every time, in this repo's own demo.
The fix
Treat a ratio change arriving at the observer as a ratio change. When
_devicePixelRationo longer matches_coreBrowserService.dpr, hand off tohandleDevicePixelRatioChange()instead of resizing the buffer on its own. It costs one comparison in the normal case, and the two paths agree whichever arrives first, since the second one to run finds the ratio already adopted and returns without doing anything.handleResizesets the canvas fromdimensions.device.canvasrather than from the size observed here, the observer fires again for that, and the rounding correction then applies with the ratios in agreement.Why not just re-apply the viewport
Calling
GlyphRenderer.handleResize()after the buffer resize would put the viewport and the buffer back in step, but it wouldn't fix the rendering. The vertex coordinates are still built fromdimensions.device, computed at the old ratio, so the terminal would come out consistently half size instead of half size in a corner. The dimensions have to be recomputed at the new ratio, which is whathandleDevicePixelRatioChangedoes by way ofhandleResize.Testing
npm run build,npm run esbuild,npm run lint-changesandnpm run test-unitare all clean.In the demo with the WebGL renderer active, this stands in for a display density change that
ScreenDprMonitordidn't see. It puts the renderer in the state it would have been in before the display got denser, then calls the observer's callback with the box the browser would report afterwards:On
masterthe terminal ends up drawn small in the corner, with_canvasat twicedimensions.device.canvasand_devicePixelRatiostill stale, and it stays there. With this change both are back in agreement immediately and the terminal renders normally.Before, on
master:After, with this change:
I couldn't find a way to write an automated test for it. The trigger is a real device pixel ratio change: Playwright's
deviceScaleFactoris fixed for the lifetime of a browser context, and the DevTools metrics override changeswindow.devicePixelRatiowithout producing adevice-pixel-content-boxchange in headless Chromium, so neither reaches this code path. Happy to add one if there's a hook I've missed.Related
#4728 and #4731 are the same surface with a different cause. There the box the observer reports disagrees with the ratio under DevTools device emulation, rather than the ratio having changed without
ScreenDprMonitornoticing. This change doesn't address those.