Skip to content

linux: Fix oversized native window content on HiDPI displays - #4208

Open
linesight wants to merge 5 commits into
chromiumembedded:masterfrom
linesight:fix-3396-ozone-x11-initial-size
Open

linux: Fix oversized native window content on HiDPI displays#4208
linesight wants to merge 5 commits into
chromiumembedded:masterfrom
linesight:fix-3396-ozone-x11-initial-size

Conversation

@linesight

Copy link
Copy Markdown
Contributor

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 the CefWindowX11 host at the pixel bounds from window_info_.bounds, but passes those same pixel dimensions as the views::Widget::InitParams::bounds, which are interpreted as DIP. DesktopWindowTreeHostLinux then scales them back up by the device scale factor, so the compositor child window ends up size * DSF pixels inside a size-pixel host. (The "fixes on resize" behavior comes from CefWindowX11::ProcessXEvent() forcing the child to the parent's pixel size on ConfigureNotify, which doesn't run at creation time.)

…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().
@linesight linesight changed the title linux: Fix Alloy-style native window content size at device scale fac… linux: Fix oversized native window content on HiDPI displays Jul 7, 2026
@magreenblatt

Copy link
Copy Markdown
Collaborator

[P1] Select the display using screen coordinates

rect is in X11 pixels and, for an embedded child window, its origin is relative to the parent. GetDisplayMatching() expects a global screen rectangle in DIP. Consequently, a parent on a secondary monitor can select the wrong display. With mixed scale factors, this applies the wrong conversion and produces oversized or undersized content—including shrinking content on a 1x monitor when the primary monitor is HiDPI.

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.
@linesight

Copy link
Copy Markdown
Contributor Author

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 Screen::GetPreferredScaleFactorForWindow() on it and reapply the DIP size with that scale. Under the hood that goes through X11Window::GetBoundsInPixels()x11::GeometryCache, which tracks bounds relative to the root and walks the parent chain summing offsets (there's a child-in-parent unit test for exactly this). So the parent offset you were worried about is folded in before the display is picked — I don't have to build the global rect myself. It's also the API that's recommended over GetDisplayMatching for per-window scaling.

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 devicePixelRatio 2). Per-monitor scale really only exists for native Wayland clients, and this native-window path is X11-only. Let me know if there's a way to exercise mixed DSF path I'm missing.

.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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@linesight

linesight commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

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 SetSize().

One snag before I add a test: I can't reproduce the origin round-trip issue from 2nd implementation. I instrumented host->GetBoundsInPixels() right where CreateHostWindow() runs, and it's (0,0) in every config I can build — the compositor is a child of CefWindowX11, which owns the actual screen origin. The pixel-pin closes that failure mode by construction, but I can't write a test that fails pre-fix and passes post-fix for something I can't make fail in the first place.

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());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

// 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());

@magreenblatt magreenblatt Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread libcef/browser/native/window_x11.cc Outdated
.window = child,
.width = size_in_pixels.width(),
.height = size_in_pixels.height(),
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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.
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.

Linux: alloy: Ozone X11 window initial size is wrong with native parent

2 participants