You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
API used: Win32 C++ API surface via windows-rs + the webview2-com crate (0.39.1) — windowed hosting (not visual/composition hosting). The repro below wraps the calls in a thin trait for our own project, but each call maps 1:1 to the raw API: create_environment → ICoreWebView2Environment::CreateCoreWebView2EnvironmentWithOptions, create_view → CreateCoreWebView2Controller, set_bounds → ICoreWebView2Controller::SetBounds, set_visible → SetIsVisible, navigate → ICoreWebView2::Navigate.
Summary
When a second ICoreWebView2Controller is created as a sibling of an already-visible, already-navigated ICoreWebView2Controller in the same parent HWND, the second controller's content never appears on screen — even though:
NavigationCompleted fires normally for the second controller.
SetIsVisible(TRUE) and SetBounds are called correctly, in the same order that works for a single controller.
The first (already-existing) controller keeps rendering and updating correctly the whole time.
A minimal, ~200-line repro (full source inlined at the bottom) reproduces this 100% of the time on the affected machine: create Controller A (covers the whole window, navigates to a solid-color data: URL), wait for it to render, then create Controller B (smaller rect, on top of A, navigates to https://example.com). Controller B's NavigationCompleted fires, but its content is never presented — the area stays exactly as Controller A painted it, indefinitely.
A single controller in its own window (no sibling) renders perfectly on the same machine, same Runtime, same code path — ruling out a generic driver/GPU/environment problem and pointing specifically at the multi-controller-per-HWND compositing path.
Why we believe this is the same root cause as #5574
#5574 ("WebView2 stops presenting host-window pixels... surface paints fresh content, OS doesn't show it") describes DirectComposition swap-chain damage-tracking incorrectly deciding the affected region's pixels are unchanged from the previous frame, so it stops issuing DComp commits even though the GPU compositor rendered fresh content. Our repro matches that description exactly — confirmed via Page.captureScreenshot over CDP in the original (more complex) app this was extracted from: the second controller's Chromium process paints the correct frame internally, but the OS never presents it. The difference is our repro isolates it to the simplest possible two-controllers-one-parent-HWND scenario, with no video element and no specific viewport-size requirement — suggesting the trigger condition may be broader than #5574's specific repro (video + bottom-anchored elements + exact viewport size), or that these are two symptoms of the same underlying damage-tracking bug.
Repro steps
Build and run the repro below (cargo run --example two_views_spike).
Observe: window opens fully solid blue (Controller A, covers 0,0–800,600).
After ~5s, Controller B is created at (100,100)–(600,450), navigated to https://example.com. NavigationCompleted fires (printed to console).
Expected: a white rectangle with "Example Domain" text appears at (100,100)–(600,450), on top of the blue.
Actual: the area stays solid blue — Controller B's content never appears, indefinitely.
Window stays open 45s total for inspection before cleanup.
What we ruled out before isolating this as a WebView2 issue
On the original (more complex) app this was extracted from, before reducing to this minimal repro, we ruled out:
Mixed-orientation multi-monitor setup (portrait + landscape) — reproduces with a single landscape monitor too.
Third-party overlay/compositor-hooking software (Wallpaper Engine, NVIDIA App/GeForce Experience in-game overlay) — reproduces with both fully disabled.
--disable-features=CalculateNativeWinOcclusion (AdditionalBrowserArguments) — no effect.
--disable-gpu-compositing (AdditionalBrowserArguments) — no effect, including in this minimal repro.
WS_CLIPCHILDREN on the parent window — no effect.
Per-Monitor-V2 DPI awareness — already declared via SetProcessDpiAwarenessContext, no effect.
Repro source (two_views_spike.rs)
Self-contained aside from windows-rs + webview2-com 0.39.1 and our own ~50-line BrowserEngine trait (happy to provide a version against the raw COM API directly if that's easier to triage — just ask).
//! Rodar com `cargo run --example two_views_spike -p browser-engine`.use std::sync::mpsc;use std::time::Duration;use idlegx_core::browser::{BrowserEngine,PixelRect,ProfileConfig,ViewEvent,WindowHandle};use windows::core::w;use windows::Win32::Foundation::{HINSTANCE,HWND,LPARAM,LRESULT,WPARAM};use windows::Win32::System::Com::{CoInitializeEx,COINIT_APARTMENTTHREADED};use windows::Win32::System::LibraryLoader::GetModuleHandleW;use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW,DefWindowProcW,DispatchMessageW,PeekMessageW,RegisterClassW,SetForegroundWindow,SetWindowPos,ShowWindow,TranslateMessage,CW_USEDEFAULT,HWND_TOP,MSG,PM_REMOVE,SET_WINDOW_POS_FLAGS,SW_SHOW,WNDCLASSW,WS_OVERLAPPEDWINDOW,};fnwait_for_nav(rx:&mpsc::Receiver<()>,label:&str){let deadline = std::time::Instant::now() + Duration::from_secs(15);loop{pump_messages_once();if rx.try_recv().is_ok(){println!("{label}: NavigationCompleted recebido.");return;}if std::time::Instant::now() > deadline {eprintln!("{label}: timeout esperando NavigationCompleted.");
std::process::exit(1);}
std::thread::sleep(Duration::from_millis(50));}}fnmain() -> windows::core::Result<()>{unsafe{CoInitializeEx(None,COINIT_APARTMENTTHREADED).ok()?;}let hwnd = create_window()?;let engine = browser_engine::WebView2Engine::new();let window = WindowHandle(hwnd.0asu64);// Controller A ("shell") — covers the whole window, navigates to a// solid color so it's visually obvious which controller is showing.println!("creating CONTROLLER A (covers whole window, blue)...");let shell_profile = ProfileConfig{profile_id:"spike-shell".to_string(),user_data_folder: std::env::temp_dir().join("idlegx-two-views-spike-shell"),proxy_server:None,proxy_username:None,proxy_password:None,};let shell_env = engine.create_environment(&shell_profile).expect("create_environment shell");let shell_view = engine.create_view(shell_env, window).expect("create_view shell");
engine
.set_bounds(shell_view,PixelRect{x:0,y:0,width:800,height:600}).expect("set_bounds shell");
engine.set_visible(shell_view,true).expect("set_visible shell");let(shell_tx, shell_rx) = mpsc::channel();
engine
.subscribe(
shell_view,Box::new(move |event:ViewEvent| {ifletViewEvent::HttpError{ code } = event {eprintln!("shell: HttpError code={code}");}ifmatches!(event,ViewEvent::NavigationCompleted{ .. }){let _ = shell_tx.send(());}}),).expect("subscribe shell");
engine.navigate(shell_view,"data:text/html,<body bgcolor=blue></body>").expect("navigate shell");wait_for_nav(&shell_rx,"shell");unsafe{let _ = ShowWindow(hwnd,SW_SHOW);let _ = SetForegroundWindow(hwnd);let _ = SetWindowPos(hwnd,Some(HWND_TOP),0,0,0,0,SET_WINDOW_POS_FLAGS(0x0001 | 0x0002));}println!("controller A alone for 5s (should be all blue now)...");let until = std::time::Instant::now() + Duration::from_secs(5);while std::time::Instant::now() < until {pump_messages_once();
std::thread::sleep(Duration::from_millis(50));}// Controller B ("content") — SEPARATE Environment, smaller rect on// top of A, navigates to example.com (white bg, visible text) — this// is the controller that reproduces the bug.println!("creating CONTROLLER B (smaller rect on top, example.com)...");let content_profile = ProfileConfig{profile_id:"spike-content".to_string(),user_data_folder: std::env::temp_dir().join("idlegx-two-views-spike-content"),proxy_server:None,proxy_username:None,proxy_password:None,};let content_env = engine.create_environment(&content_profile).expect("create_environment content");let content_view = engine.create_view(content_env, window).expect("create_view content");
engine
.set_bounds(content_view,PixelRect{x:100,y:100,width:500,height:350}).expect("set_bounds content");
engine.set_visible(content_view,true).expect("set_visible content");let(content_tx, content_rx) = mpsc::channel();
engine
.subscribe(
content_view,Box::new(move |event:ViewEvent| {ifmatches!(event,ViewEvent::NavigationCompleted{ .. }){let _ = content_tx.send(());}}),).expect("subscribe content");
engine.navigate(content_view,"https://example.com").expect("navigate content");wait_for_nav(&content_rx,"content");println!("content loaded, but STILL HIDDEN (expected, if bug reproduces) for 8s...");let until = std::time::Instant::now() + Duration::from_secs(8);while std::time::Instant::now() < until {pump_messages_once();
std::thread::sleep(Duration::from_millis(50));}// "Unstick" attempt — #5574 reports that moving/resizing the WINDOW// (not the view) makes content briefly appear before reverting ~1s// later. Testing the same here.println!("RESIZING THE WINDOW NOW (grows 40px then back) — watch if content flashes/appears...");unsafe{let _ = SetWindowPos(hwnd,Some(HWND_TOP),0,0,856,679,SET_WINDOW_POS_FLAGS(0x0002));}pump_messages_once();
std::thread::sleep(Duration::from_millis(300));unsafe{let _ = SetWindowPos(hwnd,Some(HWND_TOP),0,0,816,639,SET_WINDOW_POS_FLAGS(0x0002));}println!("keeping window open for 45s for visual inspection — LOOK NOW...");println!("EXPECTED if bug reproduces: blue background (shell) visible, rect 100,100-600,450 WITHOUT the example.com text.");println!("EXPECTED if NOT reproduced: white rect with \"Example Domain\" clearly visible over the blue.");let keep_open_until = std::time::Instant::now() + Duration::from_secs(45);while std::time::Instant::now() < keep_open_until {pump_messages_once();
std::thread::sleep(Duration::from_millis(50));}println!("cleaning up...");
engine.destroy_view(content_view).expect("destroy_view content");
engine.destroy_environment(content_env).expect("destroy_environment content");
engine.destroy_view(shell_view).expect("destroy_view shell");
engine.destroy_environment(shell_env).expect("destroy_environment shell");println!("OK — spike done.");Ok(())}fnpump_messages_once(){letmut msg = MSG::default();unsafe{whilePeekMessageW(&mut msg,None,0,0,PM_REMOVE).as_bool(){let _ = TranslateMessage(&msg);DispatchMessageW(&msg);}}}extern"system"fnwindow_proc(hwnd:HWND,msg:u32,wparam:WPARAM,lparam:LPARAM) -> LRESULT{unsafe{DefWindowProcW(hwnd, msg, wparam, lparam)}}fncreate_window() -> windows::core::Result<HWND>{unsafe{let class_name = w!("IdleGXTwoViewsSpike");let window_class = WNDCLASSW{lpfnWndProc:Some(window_proc),lpszClassName: class_name,
..Default::default()};RegisterClassW(&window_class);let instance = GetModuleHandleW(None).ok().map(|h| HINSTANCE(h.0));CreateWindowExW(Default::default(),
class_name,w!("IdleGX Two Views Spike"),WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,816,639,None,None,
instance,None,)}}
Environment
windows-rs+ thewebview2-comcrate (0.39.1) — windowed hosting (not visual/composition hosting). The repro below wraps the calls in a thin trait for our own project, but each call maps 1:1 to the raw API:create_environment→ICoreWebView2Environment::CreateCoreWebView2EnvironmentWithOptions,create_view→CreateCoreWebView2Controller,set_bounds→ICoreWebView2Controller::SetBounds,set_visible→SetIsVisible,navigate→ICoreWebView2::Navigate.Summary
When a second
ICoreWebView2Controlleris created as a sibling of an already-visible, already-navigatedICoreWebView2Controllerin the same parent HWND, the second controller's content never appears on screen — even though:NavigationCompletedfires normally for the second controller.SetIsVisible(TRUE)andSetBoundsare called correctly, in the same order that works for a single controller.A minimal, ~200-line repro (full source inlined at the bottom) reproduces this 100% of the time on the affected machine: create Controller A (covers the whole window, navigates to a solid-color
data:URL), wait for it to render, then create Controller B (smaller rect, on top of A, navigates tohttps://example.com). Controller B'sNavigationCompletedfires, but its content is never presented — the area stays exactly as Controller A painted it, indefinitely.A single controller in its own window (no sibling) renders perfectly on the same machine, same Runtime, same code path — ruling out a generic driver/GPU/environment problem and pointing specifically at the multi-controller-per-HWND compositing path.
Why we believe this is the same root cause as #5574
#5574 ("WebView2 stops presenting host-window pixels... surface paints fresh content, OS doesn't show it") describes DirectComposition swap-chain damage-tracking incorrectly deciding the affected region's pixels are unchanged from the previous frame, so it stops issuing DComp commits even though the GPU compositor rendered fresh content. Our repro matches that description exactly — confirmed via
Page.captureScreenshotover CDP in the original (more complex) app this was extracted from: the second controller's Chromium process paints the correct frame internally, but the OS never presents it. The difference is our repro isolates it to the simplest possible two-controllers-one-parent-HWND scenario, with no video element and no specific viewport-size requirement — suggesting the trigger condition may be broader than #5574's specific repro (video + bottom-anchored elements + exact viewport size), or that these are two symptoms of the same underlying damage-tracking bug.Repro steps
cargo run --example two_views_spike).https://example.com.NavigationCompletedfires (printed to console).SetWindowPos(grow then shrink back) to test the "moving/resizing briefly unsticks it" workaround reported in [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 — this does not unstick Controller B in our case.What we ruled out before isolating this as a WebView2 issue
On the original (more complex) app this was extracted from, before reducing to this minimal repro, we ruled out:
--disable-features=CalculateNativeWinOcclusion(AdditionalBrowserArguments) — no effect.--disable-gpu-compositing(AdditionalBrowserArguments) — no effect, including in this minimal repro.WS_CLIPCHILDRENon the parent window — no effect.SetProcessDpiAwarenessContext, no effect.Repro source (
two_views_spike.rs)Self-contained aside from
windows-rs+webview2-com0.39.1 and our own ~50-lineBrowserEnginetrait (happy to provide a version against the raw COM API directly if that's easier to triage — just ask).