diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index ceb5573014..046d7816b5 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -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 diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index 9493df84ac..6e962d4c39 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -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 diff --git a/patches/chromium/.patches b/patches/chromium/.patches index c2d6ef21a3..9e325cf440 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -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 diff --git a/patches/chromium/patch_osr_control_screen_info.patch b/patches/chromium/patch_osr_control_screen_info.patch new file mode 100644 index 0000000000..43f7a418f4 --- /dev/null +++ b/patches/chromium/patch_osr_control_screen_info.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: reito +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 diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 52ffc72ca2..a01a7a0655 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -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; diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 954f8988c4..baebfffb96 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -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; diff --git a/shell/browser/osr/osr_render_widget_host_view.cc b/shell/browser/osr/osr_render_widget_host_view.cc index 9014850ac2..f86993b5cd 100644 --- a/shell/browser/osr/osr_render_widget_host_view.cc +++ b/shell/browser/osr/osr_render_widget_host_view.cc @@ -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(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(); diff --git a/shell/browser/osr/osr_render_widget_host_view.h b/shell/browser/osr/osr_render_widget_host_view.h index cc498adc5d..0547ebe014 100644 --- a/shell/browser/osr/osr_render_widget_host_view.h +++ b/shell/browser/osr/osr_render_widget_host_view.h @@ -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 callback) override; - display::ScreenInfo GetScreenInfo() const override; void TransformPointToRootSurface(gfx::PointF* point) override {} gfx::Rect GetBoundsInRootWindow() override; std::optional GetDisplayFeature() override; @@ -171,6 +171,7 @@ class OffScreenRenderWidgetHostView const std::optional>& 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_; diff --git a/shell/browser/osr/osr_web_contents_view.cc b/shell/browser/osr/osr_web_contents_view.cc index 848409933a..9449e99c36 100644 --- a/shell/browser/osr/osr_web_contents_view.cc +++ b/shell/browser/osr/osr_web_contents_view.cc @@ -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() { diff --git a/shell/browser/osr/osr_web_contents_view.h b/shell/browser/osr/osr_web_contents_view.h index 047b171b63..e9d5220a07 100644 --- a/shell/browser/osr/osr_web_contents_view.h +++ b/shell/browser/osr/osr_web_contents_view.h @@ -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_; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index a6410bef40..781498dd3a 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -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"; diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index 06af2cf2d8..f5ff750ef8 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -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);