[Win] Use DXGI swapchains and DCOMP visuals in software mode

Prior to this CL, Chromium used GDI to transfer pixels from
SkiaBitmaps to the window redirection surface.

The drawback to this approach is twofold. First, alpha blending with
the Window behind the Chromium window behaves differently between
software and hardware modes. The behavior we want is the hardware
behavior where transparent pixels do NOT click through to the window
underneath. Second, this required logic to determine when to set the
WS_EX_LAYERED and the logic didn't always work. When the logic
failed, transparent bits were ignored.

Using swapchains will bring parity between the two rendering modes, and
set up the system for the eventual removal of the redirection surface.
Additionally, using Present to mark the end of the frame, causes DWM
to pick up rendering results sooner, thus reducing latency in VM
scenarios. BitBlit, on the other hand, is batched along with other GDI
commands and is only flushed after a period of inactivity.

Bug: 40176068
Change-Id: I2dbb57d6f62e816ef3a4bdde3df55e77e858c85c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6106730
Commit-Queue: Sahir Vellani <sahir.vellani@microsoft.com>
Reviewed-by: Rafael Cintron <rafael.cintron@microsoft.com>
Reviewed-by: Nico Weber <thakis@chromium.org>
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1404858}
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index 55bb7f0..7b0930d 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -126,6 +126,13 @@
 BASE_FEATURE(kDCompSurfacesForDelegatedInk,
              "DCompSurfacesForDelegatedInk",
              base::FEATURE_ENABLED_BY_DEFAULT);
+
+// If enabled, Chromium will utilize DXGI SwapChains and DComp visuals as the
+// software output device rather than GDI bit block transfer to the redirection
+// surface.
+BASE_FEATURE(kUseSwapChainForSoftwareRendering,
+             "UseSwapChainForSoftwareRendering",
+             base::FEATURE_DISABLED_BY_DEFAULT);
 #endif
 
 // Note: This feature is actively being finched (Oct, 2024).
diff --git a/components/viz/common/features.h b/components/viz/common/features.h
index 81d3ed6..db0df6ed 100644
--- a/components/viz/common/features.h
+++ b/components/viz/common/features.h
@@ -50,6 +50,7 @@
 
 #if BUILDFLAG(IS_WIN)
 VIZ_COMMON_EXPORT BASE_DECLARE_FEATURE(kDCompSurfacesForDelegatedInk);
+VIZ_COMMON_EXPORT BASE_DECLARE_FEATURE(kUseSwapChainForSoftwareRendering);
 #endif
 VIZ_COMMON_EXPORT BASE_DECLARE_FEATURE(kRenderPassDrawnRect);
 VIZ_COMMON_EXPORT BASE_DECLARE_FEATURE(kRecordSkPicture);
diff --git a/components/viz/service/BUILD.gn b/components/viz/service/BUILD.gn
index 32a1a1d..29881fa 100644
--- a/components/viz/service/BUILD.gn
+++ b/components/viz/service/BUILD.gn
@@ -467,12 +467,26 @@
       "display_embedder/software_output_device_win.h",
       "display_embedder/software_output_device_win_base.cc",
       "display_embedder/software_output_device_win_base.h",
+      "display_embedder/software_output_device_win_swapchain.cc",
+      "display_embedder/software_output_device_win_swapchain.h",
       "frame_sinks/external_begin_frame_source_win.cc",
       "frame_sinks/external_begin_frame_source_win.h",
       "gl/info_collection_gpu_service_impl.cc",
       "gl/info_collection_gpu_service_impl.h",
     ]
 
+    libs = [
+      "d3d11.lib",
+      "dxgi.lib",
+      "dcomp.lib",
+    ]
+
+    ldflags = [
+      "/DELAYLOAD:d3d11.dll",
+      "/DELAYLOAD:dxgi.dll",
+      "/DELAYLOAD:dcomp.dll",
+    ]
+
     # SkiaOutputDeviceBufferQueue doesn't support Windows.
     sources -= [
       "display_embedder/output_presenter.h",
diff --git a/components/viz/service/display_embedder/output_device_backing.cc b/components/viz/service/display_embedder/output_device_backing.cc
index eda65c7c..465333ad 100644
--- a/components/viz/service/display_embedder/output_device_backing.cc
+++ b/components/viz/service/display_embedder/output_device_backing.cc
@@ -5,6 +5,7 @@
 #include "components/viz/service/display_embedder/output_device_backing.h"
 
 #include <algorithm>
+#include <utility>
 #include <vector>
 
 #include "base/containers/contains.h"
@@ -20,6 +21,8 @@
 // bitmap for it.
 constexpr size_t kMaxBitmapSizeBytes = 4 * (16384 * 8192);
 
+constexpr DXGI_FORMAT kDXGISwapChainFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
+
 // Finds the size in bytes to hold |viewport_size| pixels. If |viewport_size| is
 // a valid size this will return true and |out_bytes| will contain the size in
 // bytes. If |viewport_size| is not a valid size then this will return false.
@@ -44,6 +47,17 @@
 }
 
 void OutputDeviceBacking::ClientResized() {
+  if (d3d11_staging_texture_) {
+    D3D11_TEXTURE2D_DESC d3d11_texture_desc;
+    d3d11_staging_texture_->GetDesc(&d3d11_texture_desc);
+    // Check if we already have a staging texture that matches the max
+    // viewport size.
+    if (GetMaxViewportSize() !=
+        gfx::Size(d3d11_texture_desc.Width, d3d11_texture_desc.Height)) {
+      d3d11_staging_texture_.Reset();
+    }
+  }
+
   // If the max viewport size doesn't change then nothing here changes.
   if (GetMaxViewportBytes() == created_shm_bytes_)
     return;
@@ -108,4 +122,89 @@
   return max_bytes;
 }
 
+gfx::Size OutputDeviceBacking::GetMaxViewportSize() const {
+  gfx::Size size;
+  for (OutputDeviceBacking::Client* client : clients_) {
+    size.SetToMax(client->GetViewportPixelSize());
+  }
+  return size;
+}
+
+HRESULT OutputDeviceBacking::GetOrCreateDXObjects(
+    Microsoft::WRL::ComPtr<ID3D11Device>* d3d11_device_out,
+    Microsoft::WRL::ComPtr<IDXGIFactory2>* dxgi_factory_out,
+    Microsoft::WRL::ComPtr<IDCompositionDevice>* dcomp_device_out) {
+  if (!d3d11_device_) {
+    DCHECK(!dxgi_factory_);
+
+    Microsoft::WRL::ComPtr<IDXGIFactory2> dxgi_factory;
+    HRESULT hr = ::CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory));
+    if (FAILED(hr)) {
+      LOG(ERROR) << "CreateDXGIFactory1 failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return hr;
+    }
+
+    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device;
+    hr = ::D3D11CreateDevice(
+        NULL, D3D_DRIVER_TYPE_WARP, nullptr, D3D11_CREATE_DEVICE_SINGLETHREADED,
+        nullptr, 0, D3D11_SDK_VERSION, &d3d11_device, nullptr, nullptr);
+    if (FAILED(hr)) {
+      LOG(ERROR) << "D3D11CreateDevice failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return hr;
+    }
+
+    Microsoft::WRL::ComPtr<IDCompositionDevice> dcomp_device;
+    hr = ::DCompositionCreateDevice(nullptr, IID_PPV_ARGS(&dcomp_device));
+    if (FAILED(hr)) {
+      LOG(ERROR) << "DCompositionCreateDevice failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return hr;
+    }
+
+    // Once all of the resources have been allocated into local variables
+    // copy them as a group to member variables so the object is never in a half
+    // baked state.
+    d3d11_device_ = std::move(d3d11_device);
+    dxgi_factory_ = std::move(dxgi_factory);
+    dcomp_device_ = std::move(dcomp_device);
+  }
+
+  *d3d11_device_out = d3d11_device_;
+  *dxgi_factory_out = dxgi_factory_;
+  *dcomp_device_out = dcomp_device_;
+  return S_OK;
+}
+
+Microsoft::WRL::ComPtr<ID3D11Texture2D>
+OutputDeviceBacking::GetOrCreateStagingTexture() {
+  if (!d3d11_staging_texture_) {
+    gfx::Size texture_dimensions = GetMaxViewportSize();
+    D3D11_TEXTURE2D_DESC d3d11_texture_desc = {
+        .Width = static_cast<UINT>(texture_dimensions.width()),
+        .Height = static_cast<UINT>(texture_dimensions.height()),
+        .MipLevels = 1,
+        .ArraySize = 1,
+        .Format = kDXGISwapChainFormat,
+        .SampleDesc = {.Count = 1, .Quality = 0},
+        .Usage = D3D11_USAGE_STAGING,
+        .BindFlags = 0,
+        .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
+        .MiscFlags = 0};
+    Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_staging_texture;
+    const HRESULT hr = d3d11_device_->CreateTexture2D(
+        &d3d11_texture_desc, nullptr, &d3d11_staging_texture);
+    if (FAILED(hr)) {
+      LOG(ERROR)
+          << "ID3D11Texture2D::CreateTexture2D (for staging texture) failed: "
+          << logging::SystemErrorCodeToString(hr);
+      return nullptr;
+    }
+    d3d11_staging_texture_ = std::move(d3d11_staging_texture);
+  }
+
+  return d3d11_staging_texture_;
+}
+
 }  // namespace viz
diff --git a/components/viz/service/display_embedder/output_device_backing.h b/components/viz/service/display_embedder/output_device_backing.h
index d287c1b..d8763a8 100644
--- a/components/viz/service/display_embedder/output_device_backing.h
+++ b/components/viz/service/display_embedder/output_device_backing.h
@@ -5,6 +5,11 @@
 #ifndef COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_OUTPUT_DEVICE_BACKING_H_
 #define COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_OUTPUT_DEVICE_BACKING_H_
 
+#include <d3d11.h>
+#include <dcomp.h>
+#include <dxgi1_3.h>
+#include <wrl/client.h>
+
 #include <memory>
 #include <vector>
 
@@ -44,7 +49,7 @@
   void UnregisterClient(Client* client);
 
   // Called when a client has resized. Clients should call Resize() after being
-  // registered when they have a valid size. Will potential invalidate
+  // registered when they have a valid size. Will potentially invalidate
   // SharedMemory and call ReleaseCanvas() on clients.
   void ClientResized();
 
@@ -58,10 +63,30 @@
   // registered clients.
   size_t GetMaxViewportBytes();
 
+  // Retrieve the ID3D11Device and IDCompositionDevice to use for presentation.
+  HRESULT GetOrCreateDXObjects(
+      Microsoft::WRL::ComPtr<ID3D11Device>* d3d11_device,
+      Microsoft::WRL::ComPtr<IDXGIFactory2>* dxgi_factory,
+      Microsoft::WRL::ComPtr<IDCompositionDevice>* dcomp_device);
+
+  // Returns the D3D11 staging texture or creates one if it doesn't exist.
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> GetOrCreateStagingTexture();
+
+  // Returns the size needed for the largest viewport from registered clients,
+  // in pixels.
+  gfx::Size GetMaxViewportSize() const;
+
  private:
   std::vector<raw_ptr<Client, VectorExperimental>> clients_;
   base::UnsafeSharedMemoryRegion region_;
   size_t created_shm_bytes_ = 0;
+
+  // The following are used to cache resources that are common to all
+  // software output device backings.
+  Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device_;
+  Microsoft::WRL::ComPtr<IDXGIFactory2> dxgi_factory_;
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_staging_texture_;
+  Microsoft::WRL::ComPtr<IDCompositionDevice> dcomp_device_;
 };
 
 }  // namespace viz
diff --git a/components/viz/service/display_embedder/output_device_backing_unittest.cc b/components/viz/service/display_embedder/output_device_backing_unittest.cc
index 09c502a..32e3fd5 100644
--- a/components/viz/service/display_embedder/output_device_backing_unittest.cc
+++ b/components/viz/service/display_embedder/output_device_backing_unittest.cc
@@ -63,6 +63,7 @@
 
   EXPECT_EQ(GetViewportSizeInBytes(client_b.viewport_size()),
             backing.GetMaxViewportBytes());
+  EXPECT_EQ(client_b.viewport_size(), backing.GetMaxViewportSize());
 }
 
 // Verify that unregistering a client works as expected.
diff --git a/components/viz/service/display_embedder/output_surface_provider_impl.cc b/components/viz/service/display_embedder/output_surface_provider_impl.cc
index 98e50a61..9186701 100644
--- a/components/viz/service/display_embedder/output_surface_provider_impl.cc
+++ b/components/viz/service/display_embedder/output_surface_provider_impl.cc
@@ -146,8 +146,13 @@
     return std::make_unique<SoftwareOutputDevice>();
 
 #if BUILDFLAG(IS_WIN)
-  return CreateSoftwareOutputDeviceWin(surface_handle, &output_device_backing_,
-                                       display_client);
+  HWND child_hwnd;
+  auto device = CreateSoftwareOutputDeviceWin(
+      surface_handle, &output_device_backing_, display_client, child_hwnd);
+  if (child_hwnd) {
+    display_client->AddChildWindowToBrowser(child_hwnd);
+  }
+  return device;
 #elif BUILDFLAG(IS_APPLE)
   return std::make_unique<SoftwareOutputDeviceMac>(task_runner_);
 #elif BUILDFLAG(IS_ANDROID)
diff --git a/components/viz/service/display_embedder/software_output_device_win.cc b/components/viz/service/display_embedder/software_output_device_win.cc
index ed90f0ed..ac6fe5e 100644
--- a/components/viz/service/display_embedder/software_output_device_win.cc
+++ b/components/viz/service/display_embedder/software_output_device_win.cc
@@ -11,7 +11,9 @@
 #include "base/trace_event/trace_event.h"
 #include "base/win/windows_version.h"
 #include "components/viz/common/display/use_layered_window.h"
+#include "components/viz/common/features.h"
 #include "components/viz/common/resources/resource_sizes.h"
+#include "components/viz/service/display_embedder/software_output_device_win_swapchain.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/system/platform_handle.h"
 #include "services/viz/privileged/mojom/compositing/layered_window_updater.mojom.h"
@@ -170,7 +172,16 @@
 std::unique_ptr<SoftwareOutputDevice> CreateSoftwareOutputDeviceWin(
     HWND hwnd,
     OutputDeviceBacking* backing,
-    mojom::DisplayClient* display_client) {
+    mojom::DisplayClient* display_client,
+    HWND& child_hwnd) {
+  child_hwnd = NULL;
+
+  if (base::FeatureList::IsEnabled(
+          features::kUseSwapChainForSoftwareRendering)) {
+    return std::make_unique<SoftwareOutputDeviceWinSwapChain>(hwnd, child_hwnd,
+                                                              backing);
+  }
+
   if (NeedsToUseLayerWindow(hwnd)) {
     DCHECK(display_client);
 
@@ -182,9 +193,9 @@
 
     return std::make_unique<SoftwareOutputDeviceWinProxy>(
         hwnd, std::move(layered_window_updater));
-  } else {
-    return std::make_unique<SoftwareOutputDeviceWinDirect>(hwnd, backing);
   }
+
+  return std::make_unique<SoftwareOutputDeviceWinDirect>(hwnd, backing);
 }
 
 }  // namespace viz
diff --git a/components/viz/service/display_embedder/software_output_device_win.h b/components/viz/service/display_embedder/software_output_device_win.h
index fe570e5..19d4d0f9 100644
--- a/components/viz/service/display_embedder/software_output_device_win.h
+++ b/components/viz/service/display_embedder/software_output_device_win.h
@@ -83,7 +83,8 @@
 VIZ_SERVICE_EXPORT std::unique_ptr<SoftwareOutputDevice>
 CreateSoftwareOutputDeviceWin(HWND hwnd,
                               OutputDeviceBacking* backing,
-                              mojom::DisplayClient* display_client);
+                              mojom::DisplayClient* display_client,
+                              HWND& child_hwnd);
 
 }  // namespace viz
 
diff --git a/components/viz/service/display_embedder/software_output_device_win_swapchain.cc b/components/viz/service/display_embedder/software_output_device_win_swapchain.cc
new file mode 100644
index 0000000..b9ede23b
--- /dev/null
+++ b/components/viz/service/display_embedder/software_output_device_win_swapchain.cc
@@ -0,0 +1,235 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/viz/service/display_embedder/software_output_device_win_swapchain.h"
+
+#include <utility>
+
+#include "base/logging.h"
+#include "mojo/public/cpp/system/platform_handle.h"
+#include "skia/ext/platform_canvas.h"
+#include "skia/ext/skia_utils_win.h"
+
+namespace viz {
+
+namespace {
+
+constexpr DXGI_FORMAT kDXGISwapChainFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
+
+D3D11_BOX ToD3D11Box(const gfx::Rect& gfx_rect) {
+  D3D11_BOX d3d11_box = {.left = static_cast<UINT>(gfx_rect.x()),
+                         .top = static_cast<UINT>(gfx_rect.y()),
+                         .front = 0,
+                         .right = static_cast<UINT>(gfx_rect.right()),
+                         .bottom = static_cast<UINT>(gfx_rect.bottom()),
+                         .back = 1};
+  return d3d11_box;
+}
+
+}  // namespace
+
+SoftwareOutputDeviceWinSwapChain::SoftwareOutputDeviceWinSwapChain(
+    HWND hwnd,
+    HWND& child_hwnd,
+    OutputDeviceBacking* output_backing)
+    : SoftwareOutputDeviceWinBase(hwnd), output_backing_(output_backing) {
+  child_window_.Initialize();
+  child_hwnd = child_window_.window();
+  output_backing_->RegisterClient(this);
+}
+
+SoftwareOutputDeviceWinSwapChain::~SoftwareOutputDeviceWinSwapChain() {
+  output_backing_->UnregisterClient(this);
+  // If DWM.exe crashes, the Chromium window will become black until the next
+  // commit. Therefore clear the root visual manually and proactively commit.
+  dcomp_root_visual_.Reset();
+  dcomp_target_.Reset();
+  if (dcomp_device_) {
+    dcomp_device_->Commit();
+  }
+}
+
+void SoftwareOutputDeviceWinSwapChain::ResizeDelegated() {
+  // Update window size.
+  if (!SetWindowPos(child_window_.window(), nullptr, 0, 0,
+                    viewport_pixel_size_.width(), viewport_pixel_size_.height(),
+                    SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOCOPYBITS |
+                        SWP_NOOWNERZORDER | SWP_NOZORDER)) {
+    return;
+  }
+
+  // If the swapchain already exists, resize it instead of creating a new one.
+  if (dxgi_swapchain_) {
+    DCHECK(d3d11_device_);
+    DCHECK(d3d11_device_context_);
+
+    HRESULT hr = dxgi_swapchain_->ResizeBuffers(2, viewport_pixel_size_.width(),
+                                                viewport_pixel_size_.height(),
+                                                kDXGISwapChainFormat, 0);
+    if (FAILED(hr)) {
+      LOG(ERROR) << "IDXGISwapChain::ResizeBuffers failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return;
+    }
+  } else {
+    // Defer the creation of DirectX related objects to when they're needed
+    // rather than on creation of the object. This allows for a retry in the
+    // following resize call if a transient error prevents their initial
+    // creation. Furthermore it prevents the object from persisting in a
+    // semi-initialized state.
+    DCHECK(!d3d11_device_);
+    DCHECK(!d3d11_device_context_);
+    Microsoft::WRL::ComPtr<IDXGIFactory2> dxgi_factory;
+    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device;
+    Microsoft::WRL::ComPtr<IDCompositionDevice> dcomp_device;
+    HRESULT hr = output_backing_->GetOrCreateDXObjects(
+        &d3d11_device, &dxgi_factory, &dcomp_device);
+    if (FAILED(hr)) {
+      return;
+    }
+
+    Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d11_device_context;
+    d3d11_device->GetImmediateContext(&d3d11_device_context);
+
+    // Set up the visual tree.
+    Microsoft::WRL::ComPtr<IDCompositionTarget> dcomp_target;
+    hr = dcomp_device->CreateTargetForHwnd(child_window_.window(), TRUE,
+                                           &dcomp_target);
+    CHECK_EQ(hr, S_OK);
+
+    Microsoft::WRL::ComPtr<IDCompositionVisual> dcomp_root_visual;
+    hr = dcomp_device->CreateVisual(&dcomp_root_visual);
+    CHECK_EQ(hr, S_OK);
+
+    hr = dcomp_target->SetRoot(dcomp_root_visual.Get());
+    CHECK_EQ(hr, S_OK);
+
+    // Create swapchain.
+    DXGI_SWAP_CHAIN_DESC1 dxgi_swapchain_desc = {
+        .Width = static_cast<UINT>(viewport_pixel_size_.width()),
+        .Height = static_cast<UINT>(viewport_pixel_size_.height()),
+        .Format = kDXGISwapChainFormat,
+        .Stereo = FALSE,
+        .SampleDesc = {.Count = 1, .Quality = 0},
+        .BufferUsage = 0,
+        .BufferCount = 2,
+        .Scaling = DXGI_SCALING_STRETCH,
+        .SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
+        // TODO(crbug.com/384897625) Consider changing alpha mode based on
+        // whether current frame has alpha.
+        .AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
+        .Flags = 0};
+    Microsoft::WRL::ComPtr<IDXGISwapChain1> dxgi_swapchain;
+    hr = dxgi_factory->CreateSwapChainForComposition(
+        d3d11_device.Get(), &dxgi_swapchain_desc, nullptr, &dxgi_swapchain);
+    if (FAILED(hr)) {
+      LOG(ERROR) << "IDXGIFactory2::CreateSwapChainForComposition failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return;
+    }
+
+    // Set swapchain as root visual content.
+    hr = dcomp_root_visual->SetContent(dxgi_swapchain.Get());
+    CHECK_EQ(hr, S_OK);
+
+    hr = dcomp_device->Commit();
+    if (FAILED(hr)) {
+      LOG(ERROR) << "IDCompositionDevice::Commit failed: "
+                 << logging::SystemErrorCodeToString(hr);
+      return;
+    }
+
+    // Once all of the resources have been allocated into local variables
+    // copy them as a group to the member variables so the object is never
+    // in a half baked state.
+    d3d11_device_ = std::move(d3d11_device);
+    d3d11_device_context_ = std::move(d3d11_device_context);
+    dxgi_swapchain_ = std::move(dxgi_swapchain);
+    dcomp_device_ = std::move(dcomp_device);
+    dcomp_target_ = std::move(dcomp_target);
+    dcomp_root_visual_ = std::move(dcomp_root_visual);
+  }
+
+  // Notify backing of successful resizing.
+  output_backing_->ClientResized();
+}
+
+SkCanvas* SoftwareOutputDeviceWinSwapChain::BeginPaintDelegated() {
+  CHECK(!d3d11_staging_texture_);
+  d3d11_staging_texture_ = output_backing_->GetOrCreateStagingTexture();
+  if (!d3d11_device_context_ || !d3d11_staging_texture_) {
+    return nullptr;
+  }
+
+  D3D11_MAPPED_SUBRESOURCE mapped_subresource{0};
+  HRESULT hr =
+      d3d11_device_context_->Map(d3d11_staging_texture_.Get(), 0,
+                                 D3D11_MAP_READ_WRITE, 0, &mapped_subresource);
+  CHECK_EQ(hr, S_OK);
+
+  D3D11_TEXTURE2D_DESC d3d11_texture_desc;
+  d3d11_staging_texture_->GetDesc(&d3d11_texture_desc);
+
+  DCHECK_LE(static_cast<unsigned int>(viewport_pixel_size_.width()),
+            d3d11_texture_desc.Width);
+  DCHECK_LE(static_cast<unsigned int>(viewport_pixel_size_.height()),
+            d3d11_texture_desc.Height);
+
+  sk_canvas_ = skia::CreatePlatformCanvasWithPixels(
+      d3d11_texture_desc.Width, d3d11_texture_desc.Height, false,
+      static_cast<uint8_t*>(mapped_subresource.pData),
+      mapped_subresource.RowPitch, skia::CRASH_ON_FAILURE);
+  return sk_canvas_.get();
+}
+
+void SoftwareOutputDeviceWinSwapChain::EndPaintDelegated(
+    const gfx::Rect& damage) {
+  if (!sk_canvas_) {
+    return;
+  }
+
+  sk_canvas_.reset();
+  // The staging texture must be non-null if there was an `sk_canvas_`.
+  CHECK(d3d11_staging_texture_);
+  d3d11_device_context_->Unmap(d3d11_staging_texture_.Get(), 0);
+
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_texture;
+  HRESULT hr = dxgi_swapchain_->GetBuffer(0, IID_PPV_ARGS(&d3d11_texture));
+  CHECK_EQ(hr, S_OK);
+
+  // Copy the newest damage rendered to the staging texture to the next rendered
+  // frame.
+  const D3D11_BOX d3d11_box = ToD3D11Box(damage);
+  d3d11_device_context_->CopySubresourceRegion(
+      d3d11_texture.Get(), 0, damage.x(), damage.y(), 0,
+      d3d11_staging_texture_.Get(), 0, &d3d11_box);
+
+  RECT damage_rect_win = damage.ToRECT();
+  DXGI_PRESENT_PARAMETERS present_parameters = {
+      .DirtyRectsCount = 1, .pDirtyRects = &damage_rect_win};
+  hr = dxgi_swapchain_->Present1(0, 0, &present_parameters);
+  d3d11_staging_texture_.Reset();
+  // DXGI_STATUS_OCCLUDED does not indicate anything wrong with the present;
+  // only that the window is not visible at present time.
+  if (FAILED(hr) && hr != DXGI_STATUS_OCCLUDED) {
+    LOG(ERROR) << "IDXGISwapChain1::Present failed: "
+               << logging::SystemErrorCodeToString(hr);
+    return;
+  }
+}
+
+void SoftwareOutputDeviceWinSwapChain::ReleaseCanvas() {
+  // |sk_canvas_| is the only thing we retain that relies on the shared staging
+  // texture from OutputDeviceBacking. We don't expect this be called between
+  // BeginPaintDelegated() and EndPaintDelegated(), however, so we just check
+  // that the canvas isn't present.
+  CHECK(!sk_canvas_);
+}
+
+const gfx::Size& SoftwareOutputDeviceWinSwapChain::GetViewportPixelSize()
+    const {
+  return viewport_pixel_size_;
+}
+
+}  // namespace viz
diff --git a/components/viz/service/display_embedder/software_output_device_win_swapchain.h b/components/viz/service/display_embedder/software_output_device_win_swapchain.h
new file mode 100644
index 0000000..9f71339
--- /dev/null
+++ b/components/viz/service/display_embedder/software_output_device_win_swapchain.h
@@ -0,0 +1,61 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SOFTWARE_OUTPUT_DEVICE_WIN_SWAPCHAIN_H_
+#define COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SOFTWARE_OUTPUT_DEVICE_WIN_SWAPCHAIN_H_
+
+#include <d3d11.h>
+#include <dcomp.h>
+#include <dxgi1_3.h>
+#include <wrl/client.h>
+
+#include <memory>
+
+#include "components/viz/service/display_embedder/output_device_backing.h"
+#include "components/viz/service/display_embedder/software_output_device_win_base.h"
+#include "ui/gl/child_window_win.h"
+
+namespace viz {
+
+// SoftwareOutputDevice implementation in which Skia draws to a DXGI swap chain.
+// Using DXGI swap chains guarantees that alpha blending happens correctly and
+// consistently with the window behind. SoftwareOutputDeviceWinSwapChain creates
+// a trivial dcomp tree that only contains a swap chain. I.e. it is not intended
+// to support video overlays or any other feature provided by
+// `SkiaOutputDeviceDComp`.
+class VIZ_SERVICE_EXPORT SoftwareOutputDeviceWinSwapChain
+    : public SoftwareOutputDeviceWinBase,
+      public OutputDeviceBacking::Client {
+ public:
+  SoftwareOutputDeviceWinSwapChain(HWND hwnd,
+                                   HWND& child_hwnd,
+                                   OutputDeviceBacking* backing);
+  ~SoftwareOutputDeviceWinSwapChain() override;
+
+  // SoftwareOutputDeviceWinBase implementation.
+  void ResizeDelegated() override;
+  SkCanvas* BeginPaintDelegated() override;
+  void EndPaintDelegated(const gfx::Rect& rect) override;
+
+  // OutputDeviceBacking::Client implementation.
+  const gfx::Size& GetViewportPixelSize() const override;
+  void ReleaseCanvas() override;
+
+ private:
+  raw_ptr<OutputDeviceBacking> const output_backing_;
+  gl::ChildWindowWin child_window_;
+  Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device_;
+  Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d11_device_context_;
+  Microsoft::WRL::ComPtr<IDXGISwapChain1> dxgi_swapchain_;
+  Microsoft::WRL::ComPtr<IDCompositionDevice> dcomp_device_;
+  Microsoft::WRL::ComPtr<IDCompositionTarget> dcomp_target_;
+  Microsoft::WRL::ComPtr<IDCompositionVisual> dcomp_root_visual_;
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_staging_texture_;
+
+  std::unique_ptr<SkCanvas> sk_canvas_;
+};
+
+}  // namespace viz
+
+#endif  // COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SOFTWARE_OUTPUT_DEVICE_WIN_SWAPCHAIN_H_
diff --git a/skia/ext/platform_canvas.cc b/skia/ext/platform_canvas.cc
index b323e59..5c949cd 100644
--- a/skia/ext/platform_canvas.cc
+++ b/skia/ext/platform_canvas.cc
@@ -36,18 +36,18 @@
   return true;
 }
 
-#if !defined(WIN32)
-
 std::unique_ptr<SkCanvas> CreatePlatformCanvasWithPixels(
     int width,
     int height,
     bool is_opaque,
     uint8_t* data,
+    size_t bytes_per_row,
     OnFailureType failureType) {
-
   SkBitmap bitmap;
-  bitmap.setInfo(SkImageInfo::MakeN32(width, height,
-      is_opaque ? kOpaque_SkAlphaType : kPremul_SkAlphaType));
+  bitmap.setInfo(
+      SkImageInfo::MakeN32(
+          width, height, is_opaque ? kOpaque_SkAlphaType : kPremul_SkAlphaType),
+      bytes_per_row);
 
   if (data) {
     bitmap.setPixels(data);
@@ -66,6 +66,4 @@
   return std::make_unique<SkCanvas>(bitmap);
 }
 
-#endif
-
 }  // namespace skia
diff --git a/skia/ext/platform_canvas.h b/skia/ext/platform_canvas.h
index 34d8b85..27979a4 100644
--- a/skia/ext/platform_canvas.h
+++ b/skia/ext/platform_canvas.h
@@ -56,18 +56,18 @@
 // Returns the NativeDrawingContext to use for native platform drawing calls.
 SK_API HDC GetNativeDrawingContext(SkCanvas* canvas);
 
-#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \
-    defined(__sun) || defined(ANDROID) || defined(__APPLE__) ||             \
-    defined(__Fuchsia__)
+#endif
 // Construct a canvas from the given memory region. The memory is not cleared
-// first. @data must be, at least, @height * StrideForWidth(@width) bytes.
+// first. `data` must be, at least, `height` * StrideForWidth(`width`) bytes if
+// `bytes_per_row` is 0. If `bytes_per_row` is non-zero, then `data` must be at
+// least `height` * `bytes_per_row`.
 SK_API std::unique_ptr<SkCanvas> CreatePlatformCanvasWithPixels(
     int width,
     int height,
     bool is_opaque,
     uint8_t* data,
+    size_t bytes_per_row,
     OnFailureType failure_type);
-#endif
 
 inline std::unique_ptr<SkCanvas> CreatePlatformCanvas(int width,
                                                       int height,
@@ -76,7 +76,7 @@
   return CreatePlatformCanvasWithSharedSection(width, height, is_opaque, 0,
                                                CRASH_ON_FAILURE);
 #else
-  return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr,
+  return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr, 0u,
                                         CRASH_ON_FAILURE);
 #endif
 }
@@ -88,7 +88,7 @@
   return CreatePlatformCanvasWithSharedSection(width, height, is_opaque, 0,
                                                RETURN_NULL_ON_FAILURE);
 #else
-  return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr,
+  return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr, 0u,
                                         RETURN_NULL_ON_FAILURE);
 #endif
 }
diff --git a/ui/surface/transport_dib.cc b/ui/surface/transport_dib.cc
index 9e7d227..bd4d3c4 100644
--- a/ui/surface/transport_dib.cc
+++ b/ui/surface/transport_dib.cc
@@ -72,7 +72,7 @@
     return nullptr;
   return skia::CreatePlatformCanvasWithPixels(w, h, opaque,
                                               static_cast<uint8_t*>(memory()),
-                                              skia::RETURN_NULL_ON_FAILURE);
+                                              0, skia::RETURN_NULL_ON_FAILURE);
 #endif
 }