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

fix: osr use correct screen info.
This commit is contained in:
reito
2026-01-23 03:58:54 +08:00
committed by GitHub
parent 1f8e4079cd
commit e3142865b2
12 changed files with 174 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

@@ -12,6 +12,16 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron.
## Planned Breaking API Changes (42.0)
### Behavior Changed: Offscreen rendering will use `1.0` as default device scale factor.
Previously, OSR used the primary display's device scale factor for rendering, which made the output frame size vary across users.
Developers had to manually calculate the correct size using `screen.getPrimaryDisplay().scaleFactor`. We now provide an optional property
`webPreferences.offscreen.deviceScaleFactor` to specify a custom value when creating an OSR window. At first, if the property is not set, it defaults
to the primary display's scale factor (preserving the old behavior). Starting from Electron 42, the default will change to a constant value of `1.0`
for more consistent output sizes.
## Planned Breaking API Changes (41.0)
### Behavior Changed: PDFs no longer create a separate WebContents

View File

@@ -144,3 +144,4 @@ fix_check_for_file_existence_before_setting_mtime.patch
fix_linux_tray_id.patch
expose_gtk_ui_platform_field.patch
fix_os_crypt_async_cookie_encryption.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 c05fac0b5fc2166daaa94d407dd9a652e6144c80..9fa69b342d5efe5b2f2b39b522c543cc62c7175b 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -678,7 +678,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

@@ -826,6 +826,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_);
}
}
@@ -864,6 +866,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;
@@ -885,7 +888,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;
@@ -1249,6 +1252,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(
src_rect, output_size, base::TimeDelta(), 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,
@@ -151,7 +152,6 @@ class OffScreenRenderWidgetHostView
base::TimeDelta timeout,
base::OnceCallback<void(const content::CopyFromSurfaceResult&)> callback)
override;
display::ScreenInfo GetScreenInfo() const override;
void TransformPointToRootSurface(gfx::PointF* point) override {}
gfx::Rect GetBoundsInRootWindow() override;
std::optional<content::DisplayFeature> GetDisplayFeature() override;
@@ -171,6 +171,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*,
@@ -293,6 +294,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

@@ -6722,6 +6722,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);
@@ -6816,6 +6817,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);