Skip to content

[Skia.Win32] WebView2 flickering, position flash, and airspace due to child HWND hosting #22694

@clairernovotny

Description

@clairernovotny

Current behavior

The Win32 (Skia) backend hosts WebView2 via a child HWND using CreateCoreWebView2ControllerAsync. This causes three distinct visual defects:

1. White flash on show/hide/recreate

Toggling WebView2 visibility or recreating the control (e.g. navigating between pages, re-templating) produces a visible white flash. This is especially noticeable against dark-themed content. The flash occurs because the child HWND surface is composited by DWM independently from the parent window — when the HWND is shown, its surface contains the default white background for one or more frames before WebView2's multi-process renderer paints real content.

Setting DefaultBackgroundColor to a dark color before navigation reduces the contrast but does not eliminate the flash — there is still a frame where the HWND surface contains the default background, not the rendered page content.

2. Position flash at screen origin

On WebView2 creation, the HWND briefly appears near the top-left corner of the primary monitor before jumping to its correct position inside the app window. This happens because:

  1. Win32NativeWebView.cs creates the HWND with HWND.Null as parent and CW_USEDEFAULT for position — making it a top-level window at the OS-chosen default position
  2. Win32NativeElementHostingExtension.AttachNativeElement reparents it via SetParent(childHwnd, parentHwnd) on a subsequent frame
  3. Between creation and reparenting, DWM composites the HWND at its initial top-level position for one or more vsync cycles

3. Airspace (z-order violation)

The WebView2 child HWND always renders on top of Skia content, regardless of the XAML visual tree z-order. Skia renders to the window DC via WGL SwapBuffers (DWM Layer 1), while child HWNDs are composited on DWM Layer 3 — which is unconditionally above Layer 1. This means:

  • XAML elements that should appear above the WebView2 (popups, tooltips, dropdowns, overlays) are hidden behind it
  • The current mitigation (SetWindowRgn in Win32NativeElementHostingExtension) provides coarse rectangular clipping but cannot handle transparency, rounded corners, or smooth edges

Expected behavior

  • No white flash when showing, hiding, or recreating WebView2
  • No position flash — WebView2 content should appear in-place at the correct position
  • XAML elements should be able to render on top of WebView2 according to their z-order in the visual tree

How to reproduce

Minimal Uno app that demonstrates all three issues:

<!-- MainPage.xaml -->
<Page x:Class="WebView2FlickerRepro.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Background="#1E1E1E">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Spacing="8" Padding="8">
            <Button Content="Toggle Visibility" Click="ToggleVisibility_Click"/>
            <Button Content="Recreate" Click="Recreate_Click"/>
            <TextBlock x:Name="StatusText" Foreground="White" VerticalAlignment="Center"/>
        </StackPanel>

        <Grid Grid.Row="1">
            <!-- WebView2 -->
            <WebView2 x:Name="MyWebView"
                       Source="https://example.com"
                       DefaultBackgroundColor="#1E1E1E"/>

            <!-- This border SHOULD render on top of the WebView2 but won't (airspace) -->
            <Border Background="#80FF0000"
                    Width="200" Height="200"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    CornerRadius="16">
                <TextBlock Text="I should be on top of WebView2"
                           Foreground="White"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"/>
            </Border>
        </Grid>
    </Grid>
</Page>
// MainPage.xaml.cs
public sealed partial class MainPage : Page
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void ToggleVisibility_Click(object sender, RoutedEventArgs e)
    {
        // Issue 1: Each toggle produces a white flash
        MyWebView.Visibility = MyWebView.Visibility == Visibility.Visible
            ? Visibility.Collapsed
            : Visibility.Visible;
        StatusText.Text = $"Visibility: {MyWebView.Visibility}";
    }

    private void Recreate_Click(object sender, RoutedEventArgs e)
    {
        // Issue 1 + 2: Recreating produces white flash AND position flash at screen origin
        var parent = (Grid)MyWebView.Parent;
        var source = MyWebView.Source;
        parent.Children.Remove(MyWebView);

        var newWebView = new WebView2
        {
            Source = source,
            DefaultBackgroundColor = Windows.UI.Color.FromArgb(255, 30, 30, 30)
        };
        Grid.SetRow(newWebView, 0);
        parent.Children.Insert(0, newWebView);
        MyWebView = newWebView;

        StatusText.Text = "Recreated";
    }
}

Steps to reproduce:

  1. Create a new Uno app targeting net10.0-desktop (Win32/Skia backend)
  2. Add the XAML and code-behind above
  3. Run on Windows 11
  4. Issue 1: Click "Toggle Visibility" rapidly — observe white flash on each toggle
  5. Issue 2: Click "Recreate" — observe the WebView2 HWND briefly flash near the top-left of the monitor before appearing in the correct position
  6. Issue 3: Observe the red semi-transparent overlay — it is hidden behind the WebView2 instead of rendering on top

Root cause analysis

All three issues stem from hosting WebView2 via CreateCoreWebView2ControllerAsync (windowed/HWND mode). This is a fundamental limitation of the Win32 HWND composition model.

DWM layer model

Windows Desktop Window Manager composites content in four layers:

Layer 4: DirectComposition topmost visuals
Layer 3: Child HWNDs          ← WebView2 lives here (always on top of parent)
Layer 2: DirectComposition non-topmost visuals
Layer 1: GDI / OpenGL window DC  ← Skia renders here (WGL SwapBuffers)

Flickering: When a child HWND is shown or created, DWM composites its surface immediately — but the surface may not yet contain rendered content (WebView2 renders in a separate process). The gap between "surface visible" and "content painted" is the flash.

Position flash: The HWND is created as a top-level window (parent: HWND.Null) then reparented. DWM composites it at the initial position for at least one frame before SetParent moves it.

Airspace: Layer 3 is unconditionally above Layer 1. No amount of z-ordering in the XAML tree can make Skia content (Layer 1) render above a child HWND (Layer 3).

Relevant source files

File Role
Win32NativeWebView.cs Creates child HWND with HWND.Null parent, calls CreateCoreWebView2ControllerAsync (windowed mode)
Win32NativeElementHostingExtension.cs Reparents via SetParent, clips via SetWindowRgn
Win32WindowWrapper.Rendering.OpenGl.cs Skia renders to window DC via WGL SwapBuffers (Layer 1)
RenderSurfaceType.cs Renderer enum: only Software and OpenGL — no DirectComposition option

Why this cannot be fixed within the HWND model

The parent process and the browser process (WebView2) paint independently, and DWM composites them independently. There is no Win32 synchronization primitive that atomically makes an HWND visible with content already painted. Workarounds (delayed visibility, DefaultBackgroundColor, WS_EX_COMPOSITED) reduce severity but cannot eliminate the flash because:

  • DefaultBackgroundColor only controls the pre-navigation background — the HWND surface is still composited before the browser process paints
  • WS_EX_COMPOSITED enables GDI double-buffering, which doesn't apply to WGL or WebView2's multi-process renderer
  • BeginDeferWindowPos/EndDeferWindowPos batch position changes but don't control paint timing
  • WM_SETREDRAW suppresses repaints temporarily but re-enabling it triggers the same flash

Proposed solution: DirectComposition rendering mode

WinUI does not have these issues because it uses CreateCoreWebView2CompositionControllerAsync instead of CreateCoreWebView2ControllerAsync. The WebView2 renders into a DirectComposition visual — not a child HWND.

The full fix would add a DirectComposition renderer to the Win32 backend where both Skia and WebView2 render into the same DComp visual tree:

Win32 Window (WS_EX_NOREDIRECTIONBITMAP)
└── DComp Target
    ├── Skia Visual: SkiaSharp → ANGLE (OpenGL ES → D3D11) → IDCompositionSurface
    └── WebView2 Visual: CoreWebView2CompositionController → RootVisualTarget
  • No child HWND — eliminates position flash and HWND lifecycle flash
  • Atomic composition — DComp commits are atomic; content appears only when ready
  • True z-ordering — Skia and WebView2 are sibling DComp visuals, z-ordered by tree position
  • No airspace problem — XAML elements can render on top of WebView2

Scope of changes (Win32 backend only)

Change File Description
Modify RenderSurfaceType.cs Add DirectComposition enum value
Modify Win32WindowWrapper.cs Use WS_EX_NOREDIRECTIONBITMAP when DComp renderer is active
Modify Win32NativeWebView.cs Use CreateCoreWebView2CompositionControllerAsync + input forwarding via SendMouseInput
Modify Win32NativeElementHostingExtension.cs DComp path: create IDCompositionVisual instead of child HWND
New Win32WindowWrapper.Rendering.DirectComposition.cs DComp renderer: D3D11 → ANGLE → Skia into IDCompositionVirtualSurface
New AngleEglBridge.cs ANGLE EGL display/context management for D3D11↔OpenGL ES bridge
New DCompInterop.cs COM interfaces for IDCompositionDevice, IDCompositionTarget, IDCompositionVisual
Dependency ANGLE native binaries libEGL.dll + libGLESv2.dll (~10MB)

No other platforms (WASM, macOS, Linux, iOS, Android) are affected. The existing Software and OpenGL renderers remain unchanged.

Prior art

Immediate partial fix (lower effort)

A smaller change that fixes flickering but NOT airspace: use CreateCoreWebView2CompositionControllerAsync with a DComp overlay, while Skia continues rendering via WGL. This avoids the ANGLE dependency entirely (~250-400 new lines, zero new binary dependencies) but WebView2 would still always render on top of Skia content (Layer 2 above Layer 1).

Environment

  • Uno.WinUI: 5.x / 6.x (Win32/Skia backend)
  • .NET: 9 / 10
  • OS: Windows 10/11
  • WebView2 Runtime: Evergreen

Related issues

Metadata

Metadata

Assignees

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