Repository: electron/electron
Pull request: fix: visual artifacts while resizing on Windows

#### Description of Change

Refs https://github.com/electron/electron/issues/36280#issuecomment-3560964534

This fully fixes the stale pixels that we saw while resizing a window on Windows. 

Manual backports of:

- [crrev.com/c/7129658](https://crrev.com/c/7129658)
- [crrev.com/c/7210913](https://crrev.com/c/7210913)
- [crrev.com/c/7115438](https://crrev.com/c/7115438)

All three patches are necessary to fix the issue.

v40 and v41 will pick up the patches through Chromium rolls.

My work on this PR was funded by [Plasticity](https://www.plasticity.xyz/).

#### Checklist

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed visual artifacts while resizing a window on Windows.


Changed files:
filename: patches/chromium/.patches
patch: @@ -144,3 +144,6 @@ expose_referrerscriptinfo_hostdefinedoptionsindex.patch
 chore_disable_protocol_handler_dcheck.patch
 fix_release_mouse_buttons_on_focus_loss_on_wayland.patch
 viz_fix_visual_artifacts_due_to_resizing_root_render_pass_with_dcomp.patch
+viz_do_not_overallocate_surface_on_initial_render.patch
+viz_create_isbufferqueuesupportedandenabled.patch
+viz_fix_visual_artifacts_while_resizing_window_with_dcomp.patch

filename: patches/chromium/viz_create_isbufferqueuesupportedandenabled.patch
patch: @@ -0,0 +1,93 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Niklas Wenzel <dev@nikwen.de>
+Date: Wed, 3 Dec 2025 17:10:02 +0100
+Subject: viz: Create IsBufferQueueSupportedAndEnabled()
+
+Manual backport of crrev.com/c/7210913
+
+Bug: 457463689
+Change-Id: I31bbaa6b5d79697c6bb5e1fc6738f6ea5a937b4f
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7210913
+Reviewed-by: Michael Tang <tangm@microsoft.com>
+Commit-Queue: David Sanders <dsanders11@ucsbalum.com>
+Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
+Cr-Commit-Position: refs/heads/main@{#1553190}
+
+diff --git a/components/viz/service/display/output_surface.cc b/components/viz/service/display/output_surface.cc
+index ff795eb057ac64f40aa842fec8e053d12f57f538..71287614c627d39f8d019889498205ab3eff2a69 100644
+--- a/components/viz/service/display/output_surface.cc
++++ b/components/viz/service/display/output_surface.cc
+@@ -21,6 +21,16 @@
+ 
+ namespace viz {
+ 
++namespace {
++
++#if BUILDFLAG(IS_WIN)
++// Use BufferQueue for the primary plane instead of a DXGI swap chain or DComp
++// surface.
++BASE_FEATURE(kBufferQueue, base::FEATURE_DISABLED_BY_DEFAULT);
++#endif
++
++}  // namespace
++
+ OutputSurface::Capabilities::Capabilities() = default;
+ OutputSurface::Capabilities::~Capabilities() = default;
+ OutputSurface::Capabilities::Capabilities(const Capabilities& capabilities) =
+@@ -94,6 +104,12 @@ bool IsDelegatedCompositingSupportedAndEnabled(
+   // Ensure we check the feature flag iff the feature is supported.
+   return features::IsDelegatedCompositingEnabled();
+ }
++
++bool IsBufferQueueSupportedAndEnabled(
++    OutputSurface::DCSupportLevel support_level) {
++  return support_level >= OutputSurface::DCSupportLevel::kDCompDynamicTexture &&
++         base::FeatureList::IsEnabled(kBufferQueue);
++}
+ #endif
+ 
+ }  // namespace viz
+diff --git a/components/viz/service/display/output_surface.h b/components/viz/service/display/output_surface.h
+index 25306ab6e18a266efdc329e4ddd81f5303033f4c..589f4c10dad9c807c9e3ce7baba63795b629435b 100644
+--- a/components/viz/service/display/output_surface.h
++++ b/components/viz/service/display/output_surface.h
+@@ -307,6 +307,9 @@ class VIZ_SERVICE_EXPORT OutputSurface {
+ // `features::IsDelegatedCompositingEnabled()`.
+ bool IsDelegatedCompositingSupportedAndEnabled(
+     OutputSurface::DCSupportLevel support_level);
++
++bool IsBufferQueueSupportedAndEnabled(
++    OutputSurface::DCSupportLevel support_level);
+ #endif
+ 
+ }  // namespace viz
+diff --git a/components/viz/service/display/skia_renderer.cc b/components/viz/service/display/skia_renderer.cc
+index e34e15dda13a183568fc3e186d3b89da1e828ad4..9b9a4c02c975799fe42b04f0d5b680274d28b09e 100644
+--- a/components/viz/service/display/skia_renderer.cc
++++ b/components/viz/service/display/skia_renderer.cc
+@@ -121,12 +121,6 @@ namespace {
+ BASE_FEATURE(kDumpWithoutCrashingOnMissingRenderPassBacking,
+              base::FEATURE_ENABLED_BY_DEFAULT);
+ 
+-#if BUILDFLAG(IS_WIN)
+-// Use BufferQueue for the primary plane instead of a DXGI swap chain or DComp
+-// surface.
+-BASE_FEATURE(kBufferQueue, base::FEATURE_DISABLED_BY_DEFAULT);
+-#endif
+-
+ // Smallest unit that impacts anti-aliasing output. We use this to determine
+ // when an exterior edge (with AA) has been clipped (no AA). The specific value
+ // was chosen to match that used by gl_renderer.
+@@ -992,10 +986,8 @@ SkiaRenderer::SkiaRenderer(const RendererSettings* settings,
+ 
+   // It's possible to use BufferQueue with DComp textures, so we can optionally
+   // enable it behind a feature flag.
+-  const bool want_buffer_queue =
+-      output_surface_->capabilities().dc_support_level >=
+-          OutputSurface::DCSupportLevel::kDCompDynamicTexture &&
+-      base::FeatureList::IsEnabled(kBufferQueue);
++  const bool want_buffer_queue = IsBufferQueueSupportedAndEnabled(
++      output_surface_->capabilities().dc_support_level);
+ #else
+   const bool want_buffer_queue = true;
+ #endif

filename: patches/chromium/viz_do_not_overallocate_surface_on_initial_render.patch
patch: @@ -0,0 +1,45 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Niklas Wenzel <dev@nikwen.de>
+Date: Wed, 3 Dec 2025 17:08:55 +0100
+Subject: viz: Do not overallocate surface on initial render
+
+Manual backport of crrev.com/c/7129658
+
+Change-Id: I0baa5865dbe66efc7b0f3f793c8e89cdc6aaa0b6
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7129658
+Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
+Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
+Reviewed-by: Michael Tang <tangm@microsoft.com>
+Cr-Commit-Position: refs/heads/main@{#1544293}
+
+diff --git a/components/viz/service/display/direct_renderer.cc b/components/viz/service/display/direct_renderer.cc
+index c03a4b3db61371021a94128f698126b5fef2f577..01abf5d36e2e117acf6f9cdc91307c9ac5616453 100644
+--- a/components/viz/service/display/direct_renderer.cc
++++ b/components/viz/service/display/direct_renderer.cc
+@@ -1081,10 +1081,13 @@ gfx::Size DirectRenderer::CalculateTextureSizeForRenderPass(
+ // buffer area and number of reallocations to quantify the trade-off.
+ gfx::Size DirectRenderer::CalculateSizeForOutputSurface(
+     const gfx::Size& requested_viewport_size) {
++  const gfx::Size surface_size = surface_size_for_swap_buffers();
++
+   // We're not able to clip back buffers if output surface does not support
+-  // clipping.
+-  if (requested_viewport_size == surface_size_for_swap_buffers() ||
++  // clipping. We don't round on the initial frame when a window is first shown.
++  if (requested_viewport_size == surface_size ||
+       !output_surface_->capabilities().supports_viewporter ||
++      surface_size.IsZero() ||
+       settings_->dont_round_texture_sizes_for_pixel_tests) {
+     device_viewport_size_ = requested_viewport_size;
+     return requested_viewport_size;
+@@ -1102,8 +1105,8 @@ gfx::Size DirectRenderer::CalculateSizeForOutputSurface(
+   // allows backings to be more easily reused during a resize operation.
+   const int request_width = requested_viewport_size.width();
+   const int request_height = requested_viewport_size.height();
+-  int surface_width = surface_size_for_swap_buffers().width();
+-  int surface_height = surface_size_for_swap_buffers().height();
++  int surface_width = surface_size.width();
++  int surface_height = surface_size.height();
+   constexpr int multiple = 256;
+ 
+   // If |request_width| or |request_height| is already a multiple of |multiple|,

filename: patches/chromium/viz_fix_visual_artifacts_while_resizing_window_with_dcomp.patch
patch: @@ -0,0 +1,122 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Niklas Wenzel <dev@nikwen.de>
+Date: Wed, 3 Dec 2025 17:42:58 +0100
+Subject: viz: Fix visual artifacts while resizing window with DComp
+
+Manual backport of crrev.com/c/7115438
+
+Bug: 457463689
+Change-Id: I9c684effe15e0b112ae533faa243e5a035e9c875
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7115438
+Commit-Queue: David Sanders <dsanders11@ucsbalum.com>
+Reviewed-by: Michael Tang <tangm@microsoft.com>
+Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
+Cr-Commit-Position: refs/heads/main@{#1553192}
+
+diff --git a/components/viz/service/display/direct_renderer.cc b/components/viz/service/display/direct_renderer.cc
+index 01abf5d36e2e117acf6f9cdc91307c9ac5616453..7e45d9ea46d0d4095169daf748e20c98db21969d 100644
+--- a/components/viz/service/display/direct_renderer.cc
++++ b/components/viz/service/display/direct_renderer.cc
+@@ -243,6 +243,31 @@ void DirectRenderer::DrawFrame(
+   current_frame()->device_viewport_size = device_viewport_size;
+   current_frame()->display_color_spaces = display_color_spaces;
+ 
++  gfx::Size surface_resource_size =
++      CalculateSizeForOutputSurface(device_viewport_size);
++
++#if BUILDFLAG(IS_WIN)
++  if (output_surface_->capabilities().clear_drawn_areas_outside_viewport &&
++      device_viewport_size != surface_resource_size) {
++    // On Windows with DirectComposition, we cannot synchronize the swap chain
++    // |Present| and the DComp |Commit| calls to take effect at the same time.
++    // (Both take effect asynchronously.) Hence, presenting a frame and changing
++    // the DComp layer clip rect can happen at different times. This can lead to
++    // ugly visual artifacts while resizing the window because it can reveal
++    // areas of the surface that are outside the viewport (crbug.com/457463689).
++    // To prevent those artifacts, we clear areas outside of the viewport with a
++    // transparent color. Transparency is expensive, so we use it only while
++    // resizing.
++    // This line gives us a transparent image format and triggers the background
++    // to be cleared in |SkiaRenderer::ClearFramebuffer|.
++    root_render_pass->has_transparent_background = true;
++    // Redraw and swap the whole surface.
++    root_render_pass->output_rect = gfx::Rect(surface_resource_size);
++    current_frame()->root_damage_rect = gfx::Rect(surface_resource_size);
++    current_frame()->device_viewport_size = surface_resource_size;
++  }
++#endif
++
+   output_surface_->SetNeedsMeasureNextDrawLatency();
+   BeginDrawingFrame();
+ 
+@@ -274,8 +299,6 @@ void DirectRenderer::DrawFrame(
+       current_frame()->display_color_spaces.GetOutputBufferFormat(
+           current_frame()->root_render_pass->content_color_usage,
+           frame_has_alpha));
+-  gfx::Size surface_resource_size =
+-      CalculateSizeForOutputSurface(device_viewport_size);
+   if (overlay_processor_) {
+     // Display transform and viewport size are needed for overlay validator on
+     // Android SurfaceControl, and viewport size is need on Windows. These need
+@@ -397,8 +420,10 @@ void DirectRenderer::DrawFrame(
+ 
+   // If we need to redraw the frame, the whole output should be considered
+   // damaged.
+-  if (needs_full_frame_redraw)
+-    current_frame()->root_damage_rect = gfx::Rect(device_viewport_size);
++  if (needs_full_frame_redraw) {
++    current_frame()->root_damage_rect =
++        gfx::Rect(current_frame()->device_viewport_size);
++  }
+ 
+   if (!skip_drawing_root_render_pass) {
+     DrawRenderPassAndExecuteCopyRequests(root_render_pass);
+diff --git a/components/viz/service/display/output_surface.h b/components/viz/service/display/output_surface.h
+index 589f4c10dad9c807c9e3ce7baba63795b629435b..641bbfc732c88141ddd929a4c334360462259ee4 100644
+--- a/components/viz/service/display/output_surface.h
++++ b/components/viz/service/display/output_surface.h
+@@ -106,6 +106,11 @@ class VIZ_SERVICE_EXPORT OutputSurface {
+ #if BUILDFLAG(IS_WIN)
+     // Whether this OutputSurface supports direct composition layers.
+     DCSupportLevel dc_support_level = DCSupportLevel::kNone;
++    // Whether to 1) clear all drawn areas outside the viewport with a
++    // transparent background color when drawing a frame and 2) swap them. This
++    // is necessary if the surface clip rect can get out of sync with the
++    // viewport size (e.g., due to a race condition).
++    bool clear_drawn_areas_outside_viewport = false;
+ #endif
+     // Whether this OutputSurface should skip DrawAndSwap(). This is true for
+     // the unified display on Chrome OS. All drawing is handled by the physical
+diff --git a/components/viz/service/display_embedder/skia_output_device_dcomp.cc b/components/viz/service/display_embedder/skia_output_device_dcomp.cc
+index 7ac9ea1cdc4416a7af8dc2a75404cbc15be6cfad..8e027d1382c5d639c1e114b8e25b6cea3af3445d 100644
+--- a/components/viz/service/display_embedder/skia_output_device_dcomp.cc
++++ b/components/viz/service/display_embedder/skia_output_device_dcomp.cc
+@@ -39,10 +39,8 @@
+ namespace viz {
+ 
+ namespace {
+-// With DirectComposition, resize surface based on root render pass size to
+-// avoid gutter which shows stale pixels.
+-BASE_FEATURE(kDirectCompositionResizeBasedOnRootSurface,
+-             base::FEATURE_ENABLED_BY_DEFAULT);
++// Apply fixes for crbug.com/457463689.
++BASE_FEATURE(kDirectCompositionResizeFixes, base::FEATURE_ENABLED_BY_DEFAULT);
+ 
+ base::TimeTicks g_last_reshape_failure = base::TimeTicks();
+ 
+@@ -161,7 +159,14 @@ SkiaOutputDeviceDComp::SkiaOutputDeviceDComp(
+   capabilities_.supports_viewporter = presenter_->SupportsViewporter();
+   capabilities_.supports_non_backed_solid_color_overlays = true;
+   capabilities_.resize_based_on_root_surface =
+-      base::FeatureList::IsEnabled(kDirectCompositionResizeBasedOnRootSurface);
++      base::FeatureList::IsEnabled(kDirectCompositionResizeFixes);
++  // With delegated compositing or a buffer queue, |Present| and |Commit| are
++  // synchronized and the clear is not needed.
++  capabilities_.clear_drawn_areas_outside_viewport =
++      base::FeatureList::IsEnabled(kDirectCompositionResizeFixes) &&
++      !IsDelegatedCompositingSupportedAndEnabled(
++          capabilities_.dc_support_level) &&
++      !IsBufferQueueSupportedAndEnabled(capabilities_.dc_support_level);
+ 
+   DCHECK(context_state_);
+   DCHECK(context_state_->gr_context() ||