fix: offscreen rendering with correct screen info. (#49681)

* fix: osr use correct screen info.

* fix: 40-x-y types

* Update breaking changes documentation

Removed details about planned breaking API changes for versions 41.0 and 42.0.
This commit is contained in:
reito
2026-02-27 00:08:34 +08:00
committed by GitHub
parent 06521fad4c
commit 589e08af80
11 changed files with 164 additions and 51 deletions

View File

@@ -94,6 +94,7 @@
The actual output pixel format and color space of the texture should refer to [`OffscreenSharedTexture`](../structures/offscreen-shared-texture.md) object in the `paint` event.
* `argb` - The requested output texture format is 8-bit unorm RGBA, with SRGB SDR color space.
* `rgbaf16` - The requested output texture format is 16-bit float RGBA, with scRGB HDR color space.
* `deviceScaleFactor` number (optional) _Experimental_ - The device scale factor of the offscreen rendering output. If not set, will use primary display's scale factor as default.
* `contextIsolation` boolean (optional) - Whether to run Electron APIs and
the specified `preload` script in a separate JavaScript context. Defaults
to `true`. The context that the `preload` script runs in will only have

View File

@@ -148,3 +148,4 @@ graphite_handle_out_of_order_recording_errors.patch
move_wayland_pointer_lock_overrides_to_common_code.patch
loaf_add_feature_to_enable_sourceurl_for_all_protocols.patch
fix_update_dbus_signal_signature_for_xdg_globalshortcuts_portal.patch
patch_osr_control_screen_info.patch

View File

@@ -0,0 +1,22 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: reito <reito@chromium.org>
Date: Wed, 29 Oct 2025 00:50:03 +0800
Subject: patch: osr control screen info
We need to override GetNewScreenInfosForUpdate to ensure the screen info
is updated correctly, instead of overriding GetScreenInfo which seems not
working.
diff --git a/content/browser/renderer_host/render_widget_host_view_base.h b/content/browser/renderer_host/render_widget_host_view_base.h
index 2fc206be84d3d41379bb26540cbf0fa4b1ba95fb..cd85325d60f0c47bbd1e15624e96e62a7f51cd47 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -685,7 +685,7 @@ class CONTENT_EXPORT RenderWidgetHostViewBase
// Generates the most current set of ScreenInfos from the current set of
// displays in the system for use in UpdateScreenInfo.
- display::ScreenInfos GetNewScreenInfosForUpdate();
+ virtual display::ScreenInfos GetNewScreenInfosForUpdate();
// Called when display properties that need to be synchronized with the
// renderer process changes. This method is called before notifying

View File

@@ -820,6 +820,8 @@ WebContents::WebContents(v8::Isolate* isolate,
&offscreen_use_shared_texture_);
use_offscreen_dict.Get(options::kSharedTexturePixelFormat,
&offscreen_shared_texture_pixel_format_);
use_offscreen_dict.Get(options::kDeviceScaleFactor,
&offscreen_device_scale_factor_);
}
}
@@ -858,6 +860,7 @@ WebContents::WebContents(v8::Isolate* isolate,
auto* view = new OffScreenWebContentsView(
false, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_,
offscreen_device_scale_factor_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
params.view = view;
params.delegate_view = view;
@@ -879,7 +882,7 @@ WebContents::WebContents(v8::Isolate* isolate,
content::WebContents::CreateParams params(session->browser_context());
auto* view = new OffScreenWebContentsView(
transparent, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_,
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
params.view = view;
params.delegate_view = view;
@@ -1243,6 +1246,7 @@ void WebContents::MaybeOverrideCreateParamsForNewWindow(
auto* view = new OffScreenWebContentsView(
false, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_,
offscreen_device_scale_factor_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
create_params->view = view;
create_params->delegate_view = view;

View File

@@ -820,6 +820,13 @@ class WebContents final : public ExclusiveAccessContext,
bool offscreen_use_shared_texture_ = false;
std::string offscreen_shared_texture_pixel_format_ = "argb";
// TODO(reito): 0.0f means the device scale factor is not set, it's a
// migration of the breaking change so that we can read the device scale
// factor from physical primary screen's info. In Electron 42, we need to set
// this to 1.0f so that the offscreen rendering use 1.0 as default when
// `deviceScaleFactor` is not specified in webPreferences.
float offscreen_device_scale_factor_ = 0.0f;
// Whether window is fullscreened by HTML5 api.
bool html_fullscreen_ = false;

View File

@@ -53,8 +53,6 @@ namespace electron {
namespace {
const float kDefaultScaleFactor = 1.0;
ui::MouseEvent UiMouseEventFromWebMouseEvent(blink::WebMouseEvent event) {
int button_flags = 0;
switch (event.button) {
@@ -96,6 +94,15 @@ ui::MouseWheelEvent UiMouseWheelEventFromWebMouseEvent(
base::ClampFloor<int>(event.delta_y)};
}
// TODO(reito): Remove this function and use default 1.0f when Electron 42.
float GetDefaultDeviceScaleFactorFromDisplayInfo() {
display::Display display =
display::Screen::Get()->GetDisplayNearestView(gfx::NativeView());
const float factor = display.device_scale_factor();
return factor > 0 ? factor : 1.0f;
}
} // namespace
class ElectronDelegatedFrameHostClient
@@ -155,6 +162,7 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
bool painting,
int frame_rate,
const OnPaintCallback& callback,
@@ -168,6 +176,7 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
offscreen_use_shared_texture_(offscreen_use_shared_texture),
offscreen_shared_texture_pixel_format_(
offscreen_shared_texture_pixel_format),
offscreen_device_scale_factor_(offscreen_device_scale_factor),
callback_(callback),
frame_rate_(frame_rate),
size_(initial_size),
@@ -184,11 +193,11 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
DCHECK(render_widget_host_);
DCHECK(!render_widget_host_->GetView());
// Initialize a screen_infos_ struct as needed, to cache the scale factor.
if (screen_infos_.screen_infos.empty()) {
UpdateScreenInfo();
// TODO(reito): Remove this when Electron 42.
if (cc::MathUtil::IsWithinEpsilon(offscreen_device_scale_factor_, 0.0f)) {
offscreen_device_scale_factor_ =
GetDefaultDeviceScaleFactorFromDisplayInfo();
}
screen_infos_.mutable_current().device_scale_factor = kDefaultScaleFactor;
delegated_frame_host_allocator_.GenerateId();
delegated_frame_host_surface_id_ =
@@ -210,15 +219,6 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
compositor_->SetDelegate(this);
compositor_->SetRootLayer(root_layer_.get());
// For offscreen rendering with format rgbaf16, we need to set correct display
// color spaces to the compositor, otherwise it won't support hdr.
if (offscreen_use_shared_texture_ &&
offscreen_shared_texture_pixel_format_ == "rgbaf16") {
gfx::DisplayColorSpaces hdr_display_color_spaces(
gfx::ColorSpace::CreateSRGBLinear(), viz::SinglePlaneFormat::kRGBA_F16);
compositor_->SetDisplayColorSpaces(hdr_display_color_spaces);
}
ResizeRootLayer(false);
render_widget_host_->SetView(this);
@@ -504,19 +504,6 @@ void OffScreenRenderWidgetHostView::CopyFromSurface(
std::move(callback));
}
display::ScreenInfo OffScreenRenderWidgetHostView::GetScreenInfo() const {
display::ScreenInfo screen_info;
screen_info.depth = 24;
screen_info.depth_per_component = 8;
screen_info.orientation_angle = 0;
screen_info.device_scale_factor = GetDeviceScaleFactor();
screen_info.orientation_type =
display::mojom::ScreenOrientation::kLandscapePrimary;
screen_info.rect = gfx::Rect(size_);
screen_info.available_rect = gfx::Rect(size_);
return screen_info;
}
gfx::Rect OffScreenRenderWidgetHostView::GetBoundsInRootWindow() {
return gfx::Rect(size_);
}
@@ -562,8 +549,8 @@ OffScreenRenderWidgetHostView::CreateViewForWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, true,
embedder_host_view->frame_rate(), callback_, render_widget_host,
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
true, embedder_host_view->frame_rate(), callback_, render_widget_host,
embedder_host_view, size());
}
@@ -971,35 +958,55 @@ void OffScreenRenderWidgetHostView::InvalidateBounds(const gfx::Rect& bounds) {
CompositeFrame(bounds);
}
display::ScreenInfos
OffScreenRenderWidgetHostView::GetNewScreenInfosForUpdate() {
display::ScreenInfo screen_info;
screen_info.depth = 24;
screen_info.depth_per_component = 8;
screen_info.orientation_angle = 0;
screen_info.orientation_type =
display::mojom::ScreenOrientation::kLandscapePrimary;
screen_info.rect = gfx::Rect(size_);
screen_info.available_rect = gfx::Rect(size_);
screen_info.device_scale_factor = offscreen_device_scale_factor_;
// When pixel format is 'rgbaf16', we need to set screen info to support HDR.
if (offscreen_use_shared_texture_ &&
offscreen_shared_texture_pixel_format_ == "rgbaf16") {
gfx::DisplayColorSpaces hdr_display_color_spaces{
gfx::ColorSpace::CreateSRGBLinear(), viz::SinglePlaneFormat::kRGBA_F16};
// The max luminance value doesn't matter so we set to a large value.
hdr_display_color_spaces.SetHDRMaxLuminanceRelative(100.0f);
screen_info.display_color_spaces = hdr_display_color_spaces;
}
display::ScreenInfos screen_infos{screen_info};
return screen_infos;
}
void OffScreenRenderWidgetHostView::ResizeRootLayer(bool force) {
SetupFrameRate(false);
display::Display display =
display::Screen::Get()->GetDisplayNearestView(GetNativeView());
const float scaleFactor = display.device_scale_factor();
float sf = GetDeviceScaleFactor();
const bool sf_did_change = scaleFactor != sf;
// Initialize a screen_infos_ struct as needed, to cache the scale factor.
if (screen_infos_.screen_infos.empty()) {
UpdateScreenInfo();
}
screen_infos_.mutable_current().device_scale_factor = scaleFactor;
auto old_screen_info = screen_infos_.current();
UpdateScreenInfo();
auto new_screen_info = screen_infos_.current();
gfx::Size size = GetViewBounds().size();
if (!force && !sf_did_change && size == root_layer()->bounds().size())
if (!force && size == root_layer()->bounds().size() &&
old_screen_info == new_screen_info)
return;
root_layer()->SetBounds(gfx::Rect(size));
const gfx::Size& size_in_pixels =
gfx::ToFlooredSize(gfx::ConvertSizeToPixels(size, sf));
auto sf = GetDeviceScaleFactor();
const gfx::Size& size_in_pixels = SizeInPixels();
if (compositor_) {
compositor_allocator_.GenerateId();
compositor_surface_id_ = compositor_allocator_.GetCurrentLocalSurfaceId();
compositor_->SetScaleAndSize(sf, size_in_pixels, compositor_surface_id_);
compositor_->SetDisplayColorSpaces(new_screen_info.display_color_spaces);
}
delegated_frame_host_allocator_.GenerateId();

View File

@@ -73,6 +73,7 @@ class OffScreenRenderWidgetHostView
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
bool painting,
int frame_rate,
const OnPaintCallback& callback,
@@ -150,7 +151,6 @@ class OffScreenRenderWidgetHostView
const gfx::Size& output_size,
base::OnceCallback<void(const viz::CopyOutputBitmapWithMetadata&)>
callback) override;
display::ScreenInfo GetScreenInfo() const override;
void TransformPointToRootSurface(gfx::PointF* point) override {}
gfx::Rect GetBoundsInRootWindow() override;
std::optional<content::DisplayFeature> GetDisplayFeature() override;
@@ -170,6 +170,7 @@ class OffScreenRenderWidgetHostView
const std::optional<std::vector<gfx::Rect>>& character_bounds) override {}
gfx::Size GetCompositorViewportPixelSize() override;
ui::Compositor* GetCompositor() override;
display::ScreenInfos GetNewScreenInfosForUpdate() override;
content::RenderWidgetHostViewBase* CreateViewForWidget(
content::RenderWidgetHost*,
@@ -292,6 +293,8 @@ class OffScreenRenderWidgetHostView
const bool transparent_;
const bool offscreen_use_shared_texture_;
const std::string offscreen_shared_texture_pixel_format_;
float offscreen_device_scale_factor_;
OnPaintCallback callback_;
OnPopupPaintCallback parent_callback_;

View File

@@ -17,11 +17,13 @@ OffScreenWebContentsView::OffScreenWebContentsView(
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
const OnPaintCallback& callback)
: transparent_(transparent),
offscreen_use_shared_texture_(offscreen_use_shared_texture),
offscreen_shared_texture_pixel_format_(
offscreen_shared_texture_pixel_format),
offscreen_device_scale_factor_(offscreen_device_scale_factor),
callback_(callback) {
#if BUILDFLAG(IS_MAC)
PlatformCreate();
@@ -116,8 +118,9 @@ OffScreenWebContentsView::CreateViewForWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, painting_, GetFrameRate(),
callback_, render_widget_host, nullptr, GetSize());
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
painting_, GetFrameRate(), callback_, render_widget_host, nullptr,
GetSize());
}
content::RenderWidgetHostViewBase*
@@ -137,9 +140,9 @@ OffScreenWebContentsView::CreateViewForChildWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, painting_,
embedder_host_view->frame_rate(), callback_, render_widget_host,
embedder_host_view, GetSize());
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
painting_, embedder_host_view->frame_rate(), callback_,
render_widget_host, embedder_host_view, GetSize());
}
void OffScreenWebContentsView::RenderViewReady() {

View File

@@ -38,6 +38,7 @@ class OffScreenWebContentsView : public content::WebContentsView,
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
const OnPaintCallback& callback);
~OffScreenWebContentsView() override;
@@ -113,6 +114,7 @@ class OffScreenWebContentsView : public content::WebContentsView,
const bool transparent_;
const bool offscreen_use_shared_texture_;
const std::string offscreen_shared_texture_pixel_format_;
const float offscreen_device_scale_factor_;
bool painting_ = true;
int frame_rate_ = 60;
OnPaintCallback callback_;

View File

@@ -186,6 +186,8 @@ inline constexpr std::string_view kUseSharedTexture = "useSharedTexture";
inline constexpr std::string_view kSharedTexturePixelFormat =
"sharedTexturePixelFormat";
inline constexpr std::string_view kDeviceScaleFactor = "deviceScaleFactor";
inline constexpr std::string_view kNodeIntegrationInSubFrames =
"nodeIntegrationInSubFrames";

View File

@@ -6788,6 +6788,7 @@ describe('BrowserWindow module', () => {
expect(data.constructor.name).to.equal('NativeImage');
expect(data.isEmpty()).to.be.false('data is empty');
const size = data.getSize();
// TODO(reito): Use scale factor 1.0f when Electron 42.
const { scaleFactor } = screen.getPrimaryDisplay();
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
@@ -6882,6 +6883,66 @@ describe('BrowserWindow module', () => {
});
});
describe('offscreen rendering with device scale factor', () => {
let w: BrowserWindow;
const scaleFactor = 1.5;
beforeEach(function () {
w = new BrowserWindow({
width: 100,
height: 100,
show: false,
webPreferences: {
backgroundThrottling: false,
offscreen: {
deviceScaleFactor: scaleFactor
}
}
});
});
afterEach(closeAllWindows);
it('creates offscreen window with correct size considering device scale factor', async () => {
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
const [, , data] = await paint;
expect(data.constructor.name).to.equal('NativeImage');
expect(data.isEmpty()).to.be.false('data is empty');
const size = data.getSize();
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
});
it('has correct screen and window sizes', async () => {
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
await once(w.webContents, 'dom-ready');
const sizes = await w.webContents.executeJavaScript(`
new Promise((resolve) => {
const screenSize = [screen.width, screen.height];
const outerSize = [window.outerWidth, window.outerHeight];
const dpr = window.devicePixelRatio;
resolve({ screenSize, outerSize, dpr });
});
`);
expect(sizes.screenSize).to.deep.equal([100, 100]);
expect(sizes.outerSize).to.deep.equal([100, 100]);
expect(sizes.dpr).to.be.equal(scaleFactor);
});
it('has correct device screen size media query result', async () => {
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
await once(w.webContents, 'dom-ready');
const query = `(device-width: ${100}px)`;
const matches = await w.webContents.executeJavaScript(`
new Promise((resolve) => {
const mediaQuery = window.matchMedia('${query}');
resolve(mediaQuery.matches);
});
`);
expect(matches).to.be.true();
});
});
describe('"transparent" option', () => {
afterEach(closeAllWindows);