linux: Fix oversized native window content on HiDPI displays - #4208
linux: Fix oversized native window content on HiDPI displays#4208linesight wants to merge 5 commits into
Conversation
…tor != 1 (fixes chromiumembedded#3396) On Ozone X11 with an Alloy-style native (non-Views) parent window, the web content was rendered larger than the host window on displays with a device scale factor != 1 (e.g. 1000x653 content inside an 800x522 window at DSF 1.25), and only corrected itself after a manual resize. CreateHostWindow() passed the pixel-sized bounds directly as the views::Widget init bounds. Views interprets InitParams::bounds as DIP, so DesktopWindowTreeHostLinux scaled them back up by the device scale factor, making the compositor child window size x DSF pixels while the CefWindowX11 host window stayed size pixels. Convert the size to DIP using the target display's device scale factor before calling Init(), so the compositor window matches the host window's pixel size. This mirrors the Windows implementation, which converts the client rect to DIP before Init().
|
[P1] Select the display using screen coordinates
Please determine the display from the host/parent window’s screen position, or translate the X11 bounds to root coordinates and match against native pixel display bounds. A mixed-DPI test with the parent on the non-primary display would cover this. |
Let Chromium resolve the target display after creating the child X11 window, then reapply the DIP size with that display's scale.
|
Thanks for the review. I went a bit differently than your suggestion — instead of computing the screen offset in CEF, I let Chromium resolve the display. The child X11 window is created first, then I query On testing: I ran it on a real two-monitor setup and content sizes correctly to the window, with a window on the secondary resolving to that display. I couldn't reproduce a differing per-monitor scale to exercise the mixed case, though — CEF builds against GTK3, whose X11 backend reports a single global scale factor, so both monitors come through at the same DSF. I even set the two monitors to different scales in the desktop settings, but on X11 that collapses to one global scale (both reported |
| .value_or(initial_device_scale_factor); | ||
| const gfx::Size dip_size = | ||
| gfx::ScaleToRoundedSize(rect.size(), 1.0f / device_scale_factor); | ||
| window_widget_->SetSize(dip_size); |
There was a problem hiding this comment.
[P1] SetSize() can reposition the embedded X11 child. DesktopWindowTreeHostPlatform::SetSize() reads the child’s root-relative pixel bounds through GetBoundsInDIP(), replaces the size, and writes the whole rectangle back through SetBoundsInDIP(). If the screen origin does not round-trip exactly through integer DIP—for example x=101 at DSF 2 becomes DIP x=50 and then pixel x=100—X11Window::SetBoundsInPixels() detects an origin change and sends that x value to ConfigureWindow, where X11 interprets it relative to the parent. The compositor child can therefore jump within or become clipped by its host. Please resize without round-tripping the screen origin, and add a regression case with the parent at a non-scale-aligned screen coordinate.
|
|
||
| #if BUILDFLAG(SUPPORTS_OZONE_X11) | ||
| #include "cef/libcef/browser/native/window_x11.h" | ||
| #include "ui/display/screen.h" |
There was a problem hiding this comment.
[P2] Add the owning target as a direct dependency. This new ui/display/screen.h include is owned by //ui/display, but //cef:libcef_static has no public dependency path to that target and has check_includes = true. Please add //ui/display to the CEF target dependencies so Linux GN include checking succeeds.
| // the correct pixel size and screen bounds. | ||
| const float initial_device_scale_factor = | ||
| display::Screen::Get()->GetPrimaryDisplay().device_scale_factor(); | ||
| const gfx::Size initial_dip_size = gfx::ScaleToRoundedSize( |
There was a problem hiding this comment.
[P3] Please apply CEF formatting. python3 tools/fix_style.py libcef/browser/native/browser_platform_delegate_native_linux.cc rewrites this declaration so the = ends the first line and gfx::ScaleToRoundedSize(...) begins the continuation line.
|
Pushed a fix for both points: display now comes from the host window's own root-relative bounds (the compositor child is always at (0,0) relative to its host, so it can't tell us anything about screen position), and the post-Init() resize pins the compositor's pixel size directly instead of round-tripping through DIP via One snag before I add a test: I can't reproduce the origin round-trip issue from 2nd implementation. I instrumented I do have a test ready for #3396 itself (oversized content on a non-scale-aligned parent). Is that what you're after, or did you want something that specifically exercises the origin round-trip? If it's the latter — any config in mind where the compositor's own bounds would actually carry a screen-relative origin? Happy to keep digging if so. |
| int largest_area = 0; | ||
| for (const display::Display& display : screen->GetAllDisplays()) { | ||
| gfx::Rect display_pixels = gfx::ScaleToEnclosingRect( | ||
| display.bounds(), display.device_scale_factor()); |
There was a problem hiding this comment.
[P1] Use the display stored native pixel bounds. display.bounds() is in Chromium logical multi-display coordinates, and its origin is not generally recoverable by multiplying it by that display device scale factor. For example, with a 1920-pixel-wide 2x primary followed by a 1x secondary, the secondary logical x can be 960 while its native x is 1920; this code reconstructs x=960 and can select the wrong display or fall back to the primary. X11 already preserves the exact geometry on display::Display. Please construct the rectangle from display.native_origin() and display.GetSizeInPixel() (as X11ScreenOzone::DisplayBoundsInPixels() does) and match against that.
| aura::WindowTreeHost* host = window_widget_->GetNativeWindow()->GetHost(); | ||
| gfx::Rect bounds_in_pixels = host->GetBoundsInPixels(); | ||
| bounds_in_pixels.set_size(rect.size()); | ||
| host->SetBoundsInPixels(bounds_in_pixels); |
There was a problem hiding this comment.
[P2] This path does not guarantee the requested pixel size. WindowTreeHostPlatform::SetBoundsInPixels() reaches X11Window::SetBoundsInPixels(), which passes the size through AdjustSizeForDisplay(). That helper deliberately returns width - 1, height - 1 when the requested size equals any monitor size, to avoid a top-level window-manager fullscreen interpretation. A monitor-sized embedded host can therefore retain a one-pixel gap until a later ConfigureNotify or manual resize. Please use a size-only X11 child configure path that bypasses the top-level adjustment (like CefWindowX11::ProcessXEvent()), or add an exact child-resize API.
…umembedded#3396) Match the target display against display.native_origin()/GetSizeInPixel() instead of scaling display.bounds(), whose DIP origin does not recover a non-primary display's pixel origin under mixed DPI. Resize the compositor child with a size-only X11 configure (CefWindowX11::SetChildSizeInPixels) instead of WindowTreeHost::SetBoundsInPixels(), whose AdjustSizeForDisplay() returns size-1 for a monitor-sized window and would leave the content one pixel short.
| // coordinates and match them against each display's native pixel bounds. This | ||
| // selects the correct display when the window is placed on a non-primary | ||
| // monitor, instead of assuming the primary display. The child X11 window | ||
| // cannot be used for this: it is created at (0,0) relative to the host and |
There was a problem hiding this comment.
[nit] Describe the bootstrap timing instead of a permanent (0,0) lookup. X11ScreenOzone asks X11Window::GetBoundsInPixels(), whose GeometryCache recursively adds parent offsets; once that cache is ready, this child resolves in root coordinates. The primary-display scale is used earlier because DesktopWindowTreeHostPlatform::GetRootTransform() has no platform_window during CreateXWindow(). Please qualify this as an initialization/bootstrap limitation in both comments; the current wording conflicts with the actual geometry path and the earlier discussion.
| // issue #3396). Pin the compositor to the host window's exact pixel size | ||
| // (|rect|) with a size-only child configure. See SetChildSizeInPixels() for | ||
| // why aura::WindowTreeHost::SetBoundsInPixels() cannot be used here. | ||
| window_x11_->SetChildSizeInPixels(rect.size()); |
There was a problem hiding this comment.
[P2] Include regression coverage for the supported X11 configuration before landing.
A concrete integration test can reuse the Linux X11 parent-window setup from tests/ceftests/chrome_child_window_unittest.cc, but create an Alloy-style browser with SetAsChild(...) and runtime_style = CEF_RUNTIME_STYLE_ALLOY. In OnAfterCreated, before triggering any resize, use browser->GetHost()->GetWindowHandle() as the CefWindowX11 host, use XQueryTree() to obtain its Ozone child, and use XGetWindowAttributes() to assert that both windows are exactly the requested 800x600 pixels. The synchronous X queries will flush the initialization requests, but they do not generate the manual parent resize that previously hid #3396.
Run that test at a supported non-unit global scale, for example:
xvfb-run -a ceftests --no-sandbox --force-device-scale-factor=1.25 \
--gtest_filter=AlloyChildWindowTest.InitialPixelSize
The test may also run at the default scale, but the 1.25 invocation is the regression configuration: before this change the Ozone child is approximately 1000x750 inside the 800x600 host; with this change both are 800x600 immediately. Keep the requested size different from the Xvfb monitor size so this test stays independent of the separate fullscreen-size workaround.
| .window = child, | ||
| .width = size_in_pixels.width(), | ||
| .height = size_in_pixels.height(), | ||
| }); |
There was a problem hiding this comment.
[nit] Reuse this exact-resize path for ConfigureNotify. ProcessXEvent() still has a second FindChild() plus ConfigureWindow implementation for the same size-only operation. Since bypassing AdjustSizeForDisplay() is now part of the correctness contract, please centralize it here so the creation and later-resize paths cannot drift; returning whether a child was found would preserve the existing notification condition.
Make CefWindowX11::SetChildSizeInPixels() return whether a child was found and configured, and reuse it for the ConfigureNotify resize in ProcessXEvent() instead of duplicating the size-only configure. Add AlloyChildWindowTest.InitialPixelSize, checking that an Alloy native X11 child browser and its compositor keep the exact requested pixel size; run under --force-device-scale-factor to exercise HiDPI. Also expand the CreateHostWindow() comments explaining why the device scale is resolved from the host window at creation time.
Fixes #3396.
On Ozone X11, an Alloy-style browser in a native (non-Views) parent window renders its web content device-scale-factor times too large on displays where DSF != 1, and only fits after a manual resize.
CreateHostWindow()creates theCefWindowX11host at the pixel bounds fromwindow_info_.bounds, but passes those same pixel dimensions as theviews::Widget::InitParams::bounds, which are interpreted as DIP.DesktopWindowTreeHostLinuxthen scales them back up by the device scale factor, so the compositor child window ends upsize * DSFpixels inside asize-pixel host. (The "fixes on resize" behavior comes fromCefWindowX11::ProcessXEvent()forcing the child to the parent's pixel size onConfigureNotify, which doesn't run at creation time.)