Skip to content

[Problem/Bug]: WebView2 stops presenting host-window pixels in narrow/tall window sizes when page contains a <video> + bottom-anchored absolute elements (surface paints fresh content, OS doesn't show it) #5574

@LemanuelPC

Description

@LemanuelPC

What happened?

In a WebView2-hosted application on Windows, the OS compositor stops presenting fresh pixels to the host window at certain window sizes when the page contains a <video> element. The page layout is correct, the content is in the DOM with visibility: visible, and Page.captureScreenshot (Chrome DevTools Protocol) captures the elements painted on the WebView2 surface — but the host window on screen shows stale or completely black pixels in the affected region.

In the minimal WPF reproduction below, with the window at 958×1000 (matching half of a 1920×1080 display, also reliably triggered by Win+Left snap), the page paints fresh content into the WebView2 surface but the host window on screen renders completely black apart from the hardware video overlay frame. Pause the video and the entire window goes black — even though the WebView2 surface still has the correct content.

The elements briefly become visible during transient activity that forces a body-level reflow (resizing the window, programmatic DOM mutations, dev-tools interaction). Roughly 1 second after activity stops, the host window reverts to the stale state. The visible video frame freezes in its previous-layout position even though the WebView2 surface has updated to the new layout — the position mismatch is itself proof of stale presentation.

Disabling Chromium GPU compositing via additionalBrowserArguments: --disable-gpu-compositing consistently eliminates the bug (moves page compositing to the CPU path, which bypasses the affected DirectComposition presentation route).

The same HTML loaded directly in Microsoft Edge browser at the identical viewport size (using --app=file://path --window-size=958,1000 to remove browser chrome and match pixel-for-pixel) renders correctly. The Chromium layer tree dumped via CDP LayerTree.compositingReasons is byte-identical between broken-WebView2 and working-Edge — so the bug is not in any Chromium logic; it's in WebView2's surface-presentation path into the host HWND.

Importance

Important. Affects any WebView2 application that combines <video> playback with absolute-positioned bottom UI bars — a very common pattern (media players, streaming clients, video editors, lyrics/subtitle viewers).

Runtime Channel

Stable release (WebView2 Runtime).

Runtime Version

147.0.3912.86 — also reproduces on Fixed-Version Runtime 146.0.3856.109. Not a recent regression.

SDK Version

Microsoft.Web.WebView2 1.0.3912.50

Framework

WPF (primary repro). Also reproduces identically in wry / Tauri 2 hosts on Windows. Both hosting frameworks use the basic CoreWebView2Controller HWND pattern, so the bug is independent of the host framework.

Operating System

Windows 11.

OS Version

Windows 11 Home, Build 26200. NVIDIA GeForce RTX 3050 Laptop, driver 32.0.15.9621 (latest as of 2026-04-12). Chromium reports optimus: false, so this isn't engaging hybrid-GPU switching code.

Repro steps

Minimal WPF host

<!-- MainWindow.xaml -->
<Window x:Class="Repro.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        Title="Repro" Width="958" Height="1000"
        WindowStartupLocation="Manual" Left="0" Top="0">
    <Grid>
        <wv2:WebView2 x:Name="webView" />
    </Grid>
</Window>
// MainWindow.xaml.cs
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Web.WebView2.Core;

namespace Repro;
public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
        Loaded += async (_, _) => await Init();
    }
    async Task Init() {
        var dataDir = Path.Combine(Path.GetTempPath(), "repro-wv2-data");
        Directory.CreateDirectory(dataDir);
        var opts = new CoreWebView2EnvironmentOptions(
            additionalBrowserArguments: "--remote-debugging-port=9224 --autoplay-policy=no-user-gesture-required");
        var env = await CoreWebView2Environment.CreateAsync(null, dataDir, opts);
        await webView.EnsureCoreWebView2Async(env);
        webView.CoreWebView2.Navigate("file:///C:/path/to/repro.html");
    }
}

Minimal HTML (repro.html) — needs any short MP4 saved next to it as video.mp4

<!doctype html>
<html><body style="margin:0;background:#000;color:#fff;overflow:hidden;font-family:system-ui">
<main style="position:relative;width:100vw;height:100vh;overflow:hidden;background:#000">

  <canvas id="hidden-canvas"
    style="position:absolute;top:0;left:0;width:100%;height:100%;
           z-index:1;pointer-events:none;visibility:hidden;"></canvas>

  <video src="video.mp4" autoplay muted loop playsinline
    style="position:absolute;top:0;left:0;width:100%;height:100%;
           object-fit:contain;z-index:2;"></video>

  <div style="position:absolute;left:3%;right:3%;bottom:5.5rem;z-index:10;
              text-align:center;font-size:2rem;font-weight:700;color:#ff4080;
              text-shadow:0 2px 6px rgba(0,0,0,0.6);pointer-events:none">
    LYRICS LINE — should always be visible above the now-playing bar
  </div>

  <div style="position:absolute;bottom:0;left:0;right:0;padding:1rem 2rem;
              background:linear-gradient(transparent,rgba(0,0,0,0.85));z-index:20;
              display:flex;justify-content:space-between;font-size:1.1rem">
    <span>NOW PLAYING — should always be visible at the bottom</span>
    <span id="t">00:00</span>
  </div>

</main>
<script>
  const c = document.getElementById('hidden-canvas');
  const sz = () => { const d = devicePixelRatio||1; c.width = innerWidth*d|0; c.height = innerHeight*d|0; };
  sz(); addEventListener('resize', sz);
  setInterval(() => { document.getElementById('t').textContent = new Date().toTimeString().slice(0,8); }, 1000);
</script>
</body></html>

Steps

  1. Build and run the WPF app. Window opens at 958×1000 on the primary monitor (any size with inner width ≤ ~1300 px and inner height ≥ ~810 px will trigger; Win+Left snap on a 1920×1080 display lands in this region).
  2. Wait for the video to load and start playing.
  3. Observe: the pink "LYRICS LINE" text and the "NOW PLAYING" bar at the bottom are not visible on screen. The video continues to render (hardware video overlay path is unaffected).
  4. Pause the video via DevTools (document.querySelector('video').pause()). The entire window goes completely black on screen — even the video frame disappears — even though Page.captureScreenshot via the remote debugging port still shows everything (lyrics, now-playing bar, paused video frame, time counter) painted on the WebView2 surface.
  5. Move or resize the host window. The page becomes momentarily visible (~1 second). As soon as activity stops, the screen returns to the stale state — unless the new dimensions take the window out of the bug zone (e.g. resize wider than ~1300 px or shorter than ~810 px inner), in which case the bug clears permanently for that size.

The bug is sticky to the steady state of the page surface in the bug-trigger dimensions: any layout change forces a fresh paint that clears the stale region briefly, then ~1 second later the OS compositor stops refreshing the page surface again.

Verification of surface-vs-screen disconnect

While the bug is active in the WPF host, in parallel:

  • Connect a CDP client to http://localhost:9224/json/version and call Page.captureScreenshot. The returned PNG shows the lyrics, now-playing bar, time counter, and paused video frame painted on the WebView2 surface.
  • Capture the same window's client-area pixels via Win32 (GetClientRect + Graphics.CopyFromScreen) without any prior CDP/DevTools/DOM activity, after a 5-second settle period. The captured PNG is almost entirely black.

Two captures, simultaneously, of the "same" surface: WebView2's internal buffer has the content, the OS doesn't present it.

Repros in Edge Browser

No. The same HTML loaded in msedge.exe --app=file:///path/to/repro.html --window-size=958,1000 --autoplay-policy=no-user-gesture-required (chromeless Edge browser at the identical inner viewport) renders correctly. Same Chromium build, same DOM, same layer tree per CDP LayerTree.compositingReasons. Only the WebView2-into-host-HWND presentation path is affected.

Regression

Don't know. Reproduces on both Runtime 146.0.3856.109 (Fixed-Version) and 147.0.3912.86 (Stable). Older runtimes not bisected.

Trigger conditions (verified by capture-pair comparison and dimension sweep)

The bug consistently appears when all three are true:

  1. The window's inner viewport width is ≤ ~1300 px AND inner viewport height is ≥ ~810 px (measured on a 1920×1080 single-monitor display at DPR 1). Outside that region the bug clears. Verified by sweeping outer width 400→1900 and outer height 400→1080 in 40-100 px steps; the bug-vs-no-bug threshold is consistent. Window position on screen does not matter — the bug reproduces at any X/Y location, in normal windowed state and Aero Snap state alike.
  2. The page has a <video> element with content loaded AND the video element's painted box exactly fills (or is at most 2 px smaller in each dimension than) the host window's client area. Whether the video is paused or playing makes no difference. The pixel-precise threshold has been verified: a 1-px inset on each side (= 2 px smaller in each dimension) still reproduces; a 2-px inset on each side (= 4 px smaller) clears it. This strongly suggests the trigger is the hardware video overlay plane being eligible for full-surface presentation when the video element matches the host window's client area to within ±2 px.
  3. The page has at least one absolutely-positioned element anchored near the bottom of the viewport.

If (1) and (3) are true but (2) is missing, the bug fires once at the initial paint after layout settles into the bug-trigger dimensions, but clears as soon as any movement / DOM mutation / resize forces a fresh paint. With (2), the bug is sticky: roughly 1 second after each layout-affecting event, the OS compositor stops issuing presents for the page surface and the visible content goes stale, even though the WebView2 GPU surface continues to receive fresh paints.

This is consistent with damage-rect optimization on the WebView2 → host-HWND DirectComposition swap chain, where the hardware video overlay plane updates regularly enough to "win" the present cycle and the rest of the page surface gets stale-skipped.

Workarounds attempted

What works:

  • additionalBrowserArguments: --disable-gpu-compositing — moves page compositing off the GPU/DirectComposition path. Hardware video decode and canvas GPU paths unaffected.
  • --disable-direct-composition — equivalent (forces software compositing path).
  • A persistent body-level fixed-position overlay element painted continuously across the affected vertical band — keeps the OS compositor refreshing that region.
  • Sizing the <video> element with even a 2-pixel inset on each side (so the video box is 4 px smaller than the host window's client area in each dimension) — bug clears entirely. With a 1-pixel inset on each side (2 px smaller) the bug is still present. The threshold is between 1 px and 2 px on each side. Concretely: .video-bg { top: 2px; left: 2px; width: calc(100% - 4px); height: calc(100% - 4px); }. The visual difference is imperceptible. The video still hardware-decodes; it's just no longer eligible for the full-surface overlay-plane fast path that triggers the bug.

What does not work:

  • --disable-features=UseSurfaceLayerForVideo — actually makes things worse (whole window blanks during video playback).
  • --disable-direct-composition-video-overlays — bug unchanged.
  • will-change: transform / transform: translateZ(0) / CSS keyframe transform animations on the affected element. (Compositor-thread only — the bug is in main-thread present.)
  • isolation: isolate on parent / affected element.
  • contain: paint / contain: layout style paint on parent.

Hypothesis

Because the Chromium layer tree is byte-identical between broken-WebView2 and working-Edge (LayerTree.compositingReasons matches), the bug isn't in Chromium's compositor logic — it's in WebView2's presentation of the GPU compositor's output to the host HWND through DirectComposition. Disabling GPU compositing routes presentation through the software path, bypassing the affected DC code.

A plausible mechanism: WebView2's DirectComposition swap-chain damage tracking decides the host window's pixels in the affected region are unchanged from the previous frame (due to composition with the hardware video overlay plane), and stops issuing DComp commits for that region — even though the GPU compositor has rendered fresh page content into the swap chain. The pixel-precise (±2 px) threshold for triggering the bug suggests an exact-match check on the video element's box vs. the host HWND's client rect, used to gate full-surface overlay-plane presentation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions