From c90bd075d401889529d52d0f1b2dd12304f121d0 Mon Sep 17 00:00:00 2001 From: FoOcean Date: Mon, 19 Oct 2026 10:18:18 -0400 Subject: [PATCH 1/3] fix(capture): keep WinIsland excluded from the liquid glass magnifier DWM can drop a window's magnifier exclusion when the window is hidden and shown again or when the magnifier resizes, so the backdrop sometimes showed the island's own glass. Re-send the exclusion list whenever the window set or visibility changes, after every resize, and every 500 ms, and warn once when the call starts failing. Co-Authored-By: Claude Opus 5.5 --- .../src/capture/magnifier.rs | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/winisland-platform-windows/src/capture/magnifier.rs b/crates/winisland-platform-windows/src/capture/magnifier.rs index c062900..6fb871a 100644 --- a/crates/winisland-platform-windows/src/capture/magnifier.rs +++ b/crates/winisland-platform-windows/src/capture/magnifier.rs @@ -18,14 +18,14 @@ use windows::Win32::UI::Magnification::{ }; use windows::Win32::UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, EnumWindows, - GetWindowThreadProcessId, LWA_ALPHA, MSG, PM_REMOVE, PeekMessageW, RegisterClassW, - SWP_NOACTIVATE, SWP_NOZORDER, SetLayeredWindowAttributes, SetWindowPos, TranslateMessage, - WINDOW_EX_STYLE, WNDCLASSW, WS_CHILD, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, - WS_POPUP, WS_VISIBLE, + GetWindowThreadProcessId, IsWindowVisible, LWA_ALPHA, MSG, PM_REMOVE, PeekMessageW, + RegisterClassW, SWP_NOACTIVATE, SWP_NOZORDER, SetLayeredWindowAttributes, SetWindowPos, + TranslateMessage, WINDOW_EX_STYLE, WNDCLASSW, WS_CHILD, WS_EX_LAYERED, WS_EX_NOACTIVATE, + WS_EX_TOOLWINDOW, WS_POPUP, WS_VISIBLE, }; use windows::core::{BOOL, PCWSTR, w}; -const FILTER_REFRESH: Duration = Duration::from_secs(1); +const FILTER_REFRESH: Duration = Duration::from_millis(500); struct State { source: Mutex, @@ -174,12 +174,13 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) } }; let mut excluded = own_windows(); - apply_filter(magnifier, &mut excluded); + let mut filter_ok = apply_filter(magnifier, &excluded, true); let _ = ready.send(Ok(host.0 as isize)); let mut size = (0, 0); let mut next = Instant::now(); let mut filter_at = Instant::now(); + let mut refilter = false; let mut message = MSG::default(); while !state.stop.load(Ordering::Acquire) { let source = *state.source.lock(); @@ -204,14 +205,14 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) size.1, SWP_NOZORDER | SWP_NOACTIVATE, ); + refilter = true; } - if filter_at.elapsed() >= FILTER_REFRESH { + let windows = own_windows(); + if refilter || windows != excluded || filter_at.elapsed() >= FILTER_REFRESH { + refilter = false; filter_at = Instant::now(); - let mut windows = own_windows(); - if windows != excluded { - excluded = windows.clone(); - apply_filter(magnifier, &mut windows); - } + excluded = windows; + filter_ok = apply_filter(magnifier, &excluded, filter_ok); } let _ = MagSetWindowSource(magnifier, source); let _ = InvalidateRect(Some(magnifier), None, false); @@ -233,30 +234,39 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) } } -fn apply_filter(magnifier: HWND, windows: &mut [HWND]) { - if windows.is_empty() { - return; +/// Re-sends the exclusion list. DWM can drop a window's exclusion when it is hidden and shown +/// again or when the magnifier resizes, so this runs on every change and on a timer. Returns +/// whether the call succeeded and warns once when it starts failing. +fn apply_filter(magnifier: HWND, windows: &[(HWND, bool)], was_ok: bool) -> bool { + let mut handles: Vec = windows.iter().map(|(hwnd, _)| *hwnd).collect(); + if handles.is_empty() { + return was_ok; } // SAFETY: The magnifier control is live on this thread and the list outlives the call. - unsafe { - let _ = MagSetWindowFilterList( + let ok = unsafe { + MagSetWindowFilterList( magnifier, MW_FILTERMODE_EXCLUDE, - windows.len() as i32, - windows.as_mut_ptr(), - ); + handles.len() as i32, + handles.as_mut_ptr(), + ) } + .as_bool(); + if was_ok && !ok { + log::warn!("Magnifier could not exclude WinIsland windows"); + } + ok } -fn own_windows() -> Vec { +fn own_windows() -> Vec<(HWND, bool)> { extern "system" fn visit(hwnd: HWND, data: LPARAM) -> BOOL { // SAFETY: data points to the Vec owned by own_windows for the duration of EnumWindows. unsafe { - let windows = &mut *(data.0 as *mut Vec); + let windows = &mut *(data.0 as *mut Vec<(HWND, bool)>); let mut process = 0; GetWindowThreadProcessId(hwnd, Some(&mut process)); if process == GetCurrentProcessId() { - windows.push(hwnd); + windows.push((hwnd, IsWindowVisible(hwnd).as_bool())); } } BOOL(1) @@ -264,7 +274,10 @@ fn own_windows() -> Vec { let mut windows = Vec::new(); // SAFETY: The callback only appends to the local Vec passed through LPARAM. unsafe { - let _ = EnumWindows(Some(visit), LPARAM(&mut windows as *mut Vec as isize)); + let _ = EnumWindows( + Some(visit), + LPARAM(&mut windows as *mut Vec<(HWND, bool)> as isize), + ); } windows } From 9990453f66dd9b4478fbfaabf1012d231871ed10 Mon Sep 17 00:00:00 2001 From: FoOcean Date: Mon, 19 Oct 2026 10:18:19 -0400 Subject: [PATCH 2/3] fix(audio): activate the fallback capture on the concrete output device The fallback spectrum opened the virtual default output, which cpal activates asynchronously; after an output switch that kept failing with RPC_E_CHANGED_MODE and logged an error every second. Resolve the default to its concrete endpoint, which activates synchronously, and log the device change and missing-config warning only once per device. Co-Authored-By: Claude Opus 5.5 --- src/core/audio.rs | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/core/audio.rs b/src/core/audio.rs index cf35f7b..b336e69 100644 --- a/src/core/audio.rs +++ b/src/core/audio.rs @@ -436,6 +436,7 @@ impl AudioProcessor { spawn_worker("audio-fallback-capture", move || { let host = cpal::default_host(); let mut current_device_name = None; + let mut announced_device_name = None; let mut current_stream: Option = None; let mut current_stream_failed: Option> = None; let mut stream_running = false; @@ -490,15 +491,18 @@ impl AudioProcessor { continue; } next_device_refresh = now + DEVICE_REFRESH_INTERVAL; - let default_device = host.default_output_device(); - let default_device_name = default_device - .as_ref() + let default_device_name = host + .default_output_device() .and_then(|d| d.description().map(|desc| desc.name().to_string()).ok()); if default_device_name != current_device_name { - log::info!( - "Audio capture: default device changed from {current_device_name:?} to {default_device_name:?}" - ); + let announce = default_device_name != announced_device_name; + if announce { + log::info!( + "Audio capture: default device changed from {announced_device_name:?} to {default_device_name:?}" + ); + announced_device_name = default_device_name.clone(); + } // Releasing old stream and session current_stream = None; @@ -506,16 +510,21 @@ impl AudioProcessor { stream_running = false; current_device_name = None; - if let Some(device) = default_device { + if let Some(device) = default_device_name + .as_deref() + .and_then(|name| output_device_named(&host, name)) + { let device_name = default_device_name .clone() .unwrap_or_else(|| "unknown".to_string()); let config = match device.default_output_config() { Ok(c) => c, Err(e) => { - log::warn!( - "Audio capture: no default output config for '{device_name}': {e:?}" - ); + if announce { + log::warn!( + "Audio capture: no default output config for '{device_name}': {e:?}" + ); + } std::thread::sleep(AUDIO_RETRY_INTERVAL); continue; } @@ -694,6 +703,16 @@ where ) } +/// Resolves the default output to its concrete endpoint, which activates synchronously; the +/// virtual default device activates asynchronously and can keep failing after a switch. +fn output_device_named(host: &cpal::Host, name: &str) -> Option { + host.output_devices().ok()?.find(|device| { + device + .description() + .is_ok_and(|description| description.name() == name) + }) +} + fn spawn_worker(name: &str, work: impl FnOnce() + Send + 'static) { if let Err(error) = std::thread::Builder::new() .name(name.to_string()) From d5ad7ee89f0fba763a92db2bcdf18d49ff1916b4 Mon Sep 17 00:00:00 2001 From: FoOcean Date: Mon, 19 Oct 2026 10:18:35 -0400 Subject: [PATCH 3/3] feat(capture): run liquid glass on Windows 10 through the magnifier image callback On Windows 10, SetWindowDisplayAffinity rejects the DirectComposition island windows (0x80070008) and Windows.Graphics.Capture cannot capture the cloaked magnifier host (0x80070057), so liquid glass had no backdrop in either mode. - When capture exclusion fails, normal mode falls back to the magnifier instead of failing. - When the magnifier host cannot be captured, the magnifier runs from a hidden, uncloaked host and delivers frames through MagSetImageScalingCallback. Frames are copied into BGRA buffers, uploaded to a D3D11 texture, and fed into the existing copy path. The rate is capped at 60 Hz and identical frames are skipped. - The callback validates the image header against the buffer size, and the source rect is clamped to the island's monitor. Co-Authored-By: Claude Opus 5.5 --- .../winisland-platform-windows/src/capture.rs | 213 ++++++++++++++++-- .../src/capture/magnifier.rs | 125 ++++++++-- 2 files changed, 301 insertions(+), 37 deletions(-) diff --git a/crates/winisland-platform-windows/src/capture.rs b/crates/winisland-platform-windows/src/capture.rs index 9af05dc..a0d0be0 100644 --- a/crates/winisland-platform-windows/src/capture.rs +++ b/crates/winisland-platform-windows/src/capture.rs @@ -23,8 +23,8 @@ use windows::{ Direct3D11::{ D3D11_BOX, D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAP_FLAG_DO_NOT_WAIT, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE, - D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, D3D11CreateDevice, - ID3D11Device, ID3D11Device1, ID3D11Device5, ID3D11DeviceContext, + D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, + D3D11CreateDevice, ID3D11Device, ID3D11Device1, ID3D11Device5, ID3D11DeviceContext, ID3D11DeviceContext4, ID3D11Fence, ID3D11Multithread, ID3D11Texture2D, }, Dxgi::{ @@ -49,6 +49,7 @@ use windows::{ use winisland_platform::{BackdropCaptureFrame, BackdropCaptureRequest, BackdropShare, Rect}; const FRAME_BUFFERS: i32 = 2; +const PIXEL_RATE_LIMIT: u32 = 60; const PROBE_STEP: usize = 2; const PROBE_ALIGN: u32 = 64; const BORDER_UNKNOWN: u8 = 0; @@ -58,9 +59,15 @@ const BORDER_REQUIRED: u8 = 3; static BORDER_ACCESS: AtomicU8 = AtomicU8::new(BORDER_UNKNOWN); +#[derive(Clone)] +enum Frame { + Window(Direct3D11CaptureFrame), + Pixels(Arc), +} + #[derive(Default)] struct Latest { - frame: Option, + frame: Option, sequence: u64, } @@ -105,6 +112,9 @@ struct Import { pub(crate) struct BackdropCapture { hwnd: HWND, magnifier_mode: Cell, + pixel_mode: Cell, + pixels: RefCell>, + upload: RefCell>, device: ID3D11Device5, context: ID3D11DeviceContext4, shared: Arc, @@ -133,14 +143,18 @@ impl BackdropCapture { let winrt_device: IDirect3DDevice = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi) } .and_then(|device| device.cast()) .map_err(|error| format!("WinRT Direct3D device creation failed: {error}"))?; - let magnifier_mode = visible_to_capture; - if !magnifier_mode { - exclude_from_capture(hwnd)?; + let mut magnifier_mode = visible_to_capture; + if !magnifier_mode && let Err(error) = exclude_from_capture(hwnd) { + log::warn!("{error}; the liquid glass backdrop uses the magnifier instead"); + magnifier_mode = true; } let d3d = AgileContext(device.clone().into(), context.clone().into()); let capture = Self { hwnd, magnifier_mode: Cell::new(magnifier_mode), + pixel_mode: Cell::new(false), + pixels: RefCell::new(None), + upload: RefCell::new(None), device, context, shared: Arc::new(Shared { @@ -172,20 +186,30 @@ impl BackdropCapture { log::warn!("Backdrop capture restart failed: {error}"); return None; } - let origin = self.session.borrow().as_ref()?.origin.get(); - let interest = Rect { - left: request.interest.left - origin.0, - top: request.interest.top - origin.1, - right: request.interest.right - origin.0, - bottom: request.interest.bottom - origin.1, - }; - *self.shared.interest.lock() = interest; + let session_origin = self + .session + .borrow() + .as_ref() + .map(|session| session.origin.get()); + if let Some(origin) = session_origin { + *self.shared.interest.lock() = relative(request.interest, origin); + } + if let Some(magnifier) = self.pixels.borrow().as_ref() + && let Some(source) = monitor_clamped(self.hwnd, request.interest) + { + magnifier.set_source(source); + } self.shared.pending.store(false, Ordering::Release); self.ensure_import(request.share)?; let (frame, sequence) = { let latest = self.shared.latest.lock(); (latest.frame.clone()?, latest.sequence) }; + let origin = match &frame { + Frame::Pixels(pixels) => pixels.origin, + Frame::Window(_) => session_origin?, + }; + let interest = relative(request.interest, origin); let key = ( request.share.generation, sequence, @@ -207,14 +231,22 @@ impl BackdropCapture { fn copy( &self, - frame: &Direct3D11CaptureFrame, + frame: &Frame, request: BackdropCaptureRequest, key: (u64, u64, i32, i32), ) -> Option<()> { let import = self.import.borrow(); let import = import.as_ref()?; - let source = frame_texture(frame).ok()?; - let content = frame.ContentSize().ok()?; + let (source, content) = match frame { + Frame::Window(frame) => (frame_texture(frame).ok()?, frame.ContentSize().ok()?), + Frame::Pixels(pixels) => ( + self.upload_pixels(pixels)?, + SizeInt32 { + Width: pixels.width as i32, + Height: pixels.height as i32, + }, + ), + }; let (local_x, local_y) = (key.2, key.3); let left = local_x.max(0); let top = local_y.max(0); @@ -267,6 +299,59 @@ impl BackdropCapture { Some(()) } + fn upload_pixels(&self, pixels: &magnifier::PixelFrame) -> Option { + if self + .upload + .borrow() + .as_ref() + .is_none_or(|(_, width, height)| *width < pixels.width || *height < pixels.height) + { + let desc = D3D11_TEXTURE2D_DESC { + Width: pixels.width.max(1), + Height: pixels.height.max(1), + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + ..Default::default() + }; + let mut texture = None; + // SAFETY: The descriptor is initialized and the output Option is owned by this call. + if let Err(error) = + unsafe { self.device.CreateTexture2D(&desc, None, Some(&mut texture)) } + { + log::warn!("Backdrop upload texture failed: {error}"); + return None; + } + *self.upload.borrow_mut() = Some((texture?, desc.Width, desc.Height)); + } + let texture = self.upload.borrow().as_ref()?.0.clone(); + let region = D3D11_BOX { + left: 0, + top: 0, + front: 0, + right: pixels.width, + bottom: pixels.height, + back: 1, + }; + // SAFETY: The box fits the texture and `pixels` holds `height` rows of `width` BGRA texels. + unsafe { + self.context.UpdateSubresource( + &texture, + 0, + Some(®ion), + pixels.pixels.as_ptr().cast(), + pixels.width * 4, + 0, + ); + } + Some(texture) + } + fn ensure_import(&self, share: BackdropShare) -> Option<()> { if self .import @@ -317,6 +402,9 @@ impl BackdropCapture { } fn ensure_session(&self, rate_hz: u32) -> Result<(), String> { + if self.pixel_mode.get() { + return self.ensure_pixel_session(rate_hz); + } if self.magnifier_mode.get() { match self.ensure_magnifier_session(rate_hz) { Ok(()) => return Ok(()), @@ -383,12 +471,21 @@ impl BackdropCapture { self.session.borrow_mut().take(); *self.shared.latest.lock() = Latest::default(); self.copied.set(None); - let magnifier = magnifier::Magnifier::start(rect, rate_hz)?; + let magnifier = magnifier::Magnifier::start(rect, rate_hz, None)?; let interop = windows::core::factory::() .map_err(|error| format!("Capture interop is unavailable: {error}"))?; // SAFETY: The magnifier host is a live window owned by this process. - let item: GraphicsCaptureItem = unsafe { interop.CreateForWindow(magnifier.host()) } - .map_err(|error| format!("Magnifier capture item failed: {error}"))?; + let item: GraphicsCaptureItem = match unsafe { interop.CreateForWindow(magnifier.host()) } { + Ok(item) => item, + Err(error) => { + log::info!( + "Magnifier window capture is unavailable ({error}); reading magnifier frames directly" + ); + drop(magnifier); + self.pixel_mode.set(true); + return self.ensure_pixel_session(rate_hz); + } + }; // SAFETY: hwnd is the live overlay window; the call only reads its placement. let monitor = unsafe { MonitorFromWindow(self.hwnd, MONITOR_DEFAULTTONEAREST) }; let session = Session::open( @@ -405,6 +502,28 @@ impl BackdropCapture { } } +impl BackdropCapture { + fn ensure_pixel_session(&self, rate_hz: u32) -> Result<(), String> { + let rate_hz = rate_hz.min(PIXEL_RATE_LIMIT); + if let Some(magnifier) = self.pixels.borrow().as_ref() { + magnifier.set_rate(rate_hz); + return Ok(()); + } + self.session.borrow_mut().take(); + *self.shared.latest.lock() = Latest::default(); + self.copied.set(None); + let weak = Arc::downgrade(&self.shared); + let sink: magnifier::FrameSink = Box::new(move |frame| { + if let Some(shared) = weak.upgrade() { + shared.receive_pixels(frame); + } + }); + let magnifier = magnifier::Magnifier::start(window_rect(self.hwnd)?, rate_hz, Some(sink))?; + *self.pixels.borrow_mut() = Some(magnifier); + Ok(()) + } +} + impl Session { fn start(monitor: HMONITOR, shared: &Arc, rate_hz: u32) -> Result { let mut info = MONITORINFO { @@ -499,6 +618,25 @@ impl Drop for Session { } impl Shared { + fn receive_pixels(&self, frame: magnifier::PixelFrame) { + { + let mut latest = self.latest.lock(); + if let Some(Frame::Pixels(previous)) = latest.frame.as_ref() + && previous.origin == frame.origin + && previous.width == frame.width + && previous.height == frame.height + && previous.pixels == frame.pixels + { + return; + } + latest.frame = Some(Frame::Pixels(Arc::new(frame))); + latest.sequence += 1; + } + if !self.pending.swap(true, Ordering::AcqRel) { + crate::window::wake(); + } + } + fn probe_changed(&self, frame: &Direct3D11CaptureFrame, interest: Rect) -> bool { let content = *self.content.lock(); let rect = Rect { @@ -658,7 +796,7 @@ impl Shared { || (unknown && self.probe_changed(&frame, interest)); { let mut latest = self.latest.lock(); - latest.frame = Some(frame); + latest.frame = Some(Frame::Window(frame)); if relevant { latest.sequence += 1; } @@ -672,6 +810,7 @@ impl Shared { impl Drop for BackdropCapture { fn drop(&mut self) { self.session.borrow_mut().take(); + self.pixels.borrow_mut().take(); *self.shared.latest.lock() = Latest::default(); self.import.borrow_mut().take(); // SAFETY: hwnd may already be destroyed; the call fails harmlessly in that case. @@ -839,6 +978,38 @@ fn exclude_from_capture(hwnd: HWND) -> Result<(), String> { .map_err(|error| format!("Capture exclusion failed: {error}")) } +/// Clips a screen rectangle to the overlay's monitor; the magnifier image callback is only fed +/// on-screen sources. +fn monitor_clamped(hwnd: HWND, rect: Rect) -> Option { + // SAFETY: hwnd is the live overlay window; the call only reads its placement. + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + let mut info = MONITORINFO { + cbSize: size_of::() as u32, + ..Default::default() + }; + // SAFETY: monitor came from MonitorFromWindow and info is a correctly sized output. + if !unsafe { GetMonitorInfoW(monitor, &mut info) }.as_bool() { + return None; + } + let bounds = info.rcMonitor; + let clamped = RECT { + left: rect.left.max(bounds.left), + top: rect.top.max(bounds.top), + right: rect.right.min(bounds.right), + bottom: rect.bottom.min(bounds.bottom), + }; + (clamped.right > clamped.left && clamped.bottom > clamped.top).then_some(clamped) +} + +fn relative(rect: Rect, origin: (i32, i32)) -> Rect { + Rect { + left: rect.left - origin.0, + top: rect.top - origin.1, + right: rect.right - origin.0, + bottom: rect.bottom - origin.1, + } +} + fn window_rect(hwnd: HWND) -> Result { let mut rect = RECT::default(); // SAFETY: hwnd is the live overlay window and rect is a valid output. diff --git a/crates/winisland-platform-windows/src/capture/magnifier.rs b/crates/winisland-platform-windows/src/capture/magnifier.rs index 6fb871a..fd105f9 100644 --- a/crates/winisland-platform-windows/src/capture/magnifier.rs +++ b/crates/winisland-platform-windows/src/capture/magnifier.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; +use std::ffi::c_void; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::thread::JoinHandle; @@ -6,15 +8,16 @@ use std::time::{Duration, Instant}; use parking_lot::Mutex; use windows::Win32::Foundation::{COLORREF, HWND, LPARAM, LRESULT, RECT, WPARAM}; use windows::Win32::Graphics::Dwm::{DWMWA_CLOAK, DwmSetWindowAttribute}; -use windows::Win32::Graphics::Gdi::InvalidateRect; +use windows::Win32::Graphics::Gdi::{HRGN, InvalidateRect}; use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::System::Threading::GetCurrentProcessId; use windows::Win32::UI::HiDpi::{ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetThreadDpiAwarenessContext, }; use windows::Win32::UI::Magnification::{ - MAGTRANSFORM, MW_FILTERMODE_EXCLUDE, MagInitialize, MagSetWindowFilterList, MagSetWindowSource, - MagSetWindowTransform, MagUninitialize, WC_MAGNIFIERW, + MAGIMAGEHEADER, MAGTRANSFORM, MW_FILTERMODE_EXCLUDE, MagInitialize, MagSetImageScalingCallback, + MagSetWindowFilterList, MagSetWindowSource, MagSetWindowTransform, MagUninitialize, + WC_MAGNIFIERW, }; use windows::Win32::UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, EnumWindows, @@ -27,14 +30,31 @@ use windows::core::{BOOL, PCWSTR, w}; const FILTER_REFRESH: Duration = Duration::from_millis(500); +/// One frame from the magnifier image callback as tightly packed BGRA rows (the header reports +/// RGBA, but the bytes are BGRA). +pub(super) struct PixelFrame { + pub(super) pixels: Vec, + pub(super) width: u32, + pub(super) height: u32, + pub(super) origin: (i32, i32), +} + +pub(super) type FrameSink = Box; + +thread_local! { + static SINK: RefCell> = const { RefCell::new(None) }; +} + struct State { source: Mutex, rate_hz: AtomicU32, stop: AtomicBool, } -/// Cloaked magnifier window that DWM fills with the desktop beneath `source`, without this -/// process's windows; Windows.Graphics.Capture reads it as an ordinary window. +/// Magnifier that renders the desktop beneath `source` without this process's windows. With a +/// sink, frames arrive through the image callback from a hidden host, which works where +/// Windows.Graphics.Capture cannot read cloaked windows (Windows 10); otherwise the host is +/// cloaked and captured as an ordinary window. pub(super) struct Magnifier { state: Arc, host: HWND, @@ -42,7 +62,11 @@ pub(super) struct Magnifier { } impl Magnifier { - pub(super) fn start(source: RECT, rate_hz: u32) -> Result { + pub(super) fn start( + source: RECT, + rate_hz: u32, + sink: Option, + ) -> Result { let state = Arc::new(State { source: Mutex::new(source), rate_hz: AtomicU32::new(rate_hz.max(1)), @@ -52,7 +76,7 @@ impl Magnifier { let thread_state = state.clone(); let thread = std::thread::Builder::new() .name("liquid-magnifier".into()) - .spawn(move || run(thread_state, sender)) + .spawn(move || run(thread_state, sender, sink)) .map_err(|error| format!("Magnifier thread failed: {error}"))?; match receiver.recv() { Ok(Ok(host)) => Ok(Self { @@ -98,7 +122,59 @@ extern "system" fn host_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARA unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } } -fn run(state: Arc, ready: std::sync::mpsc::Sender>) { +unsafe extern "system" fn on_frame( + _hwnd: HWND, + source: *mut c_void, + header: MAGIMAGEHEADER, + _destination: *mut c_void, + _destination_header: MAGIMAGEHEADER, + _unclipped: RECT, + _clipped: RECT, + _dirty: HRGN, +) -> BOOL { + let row = header.width as usize * 4; + let stride = header.stride as usize; + let height = header.height as usize; + let end = (header.offset as usize) + .saturating_add(stride.saturating_mul(height.saturating_sub(1))) + .saturating_add(row); + if source.is_null() || row == 0 || height == 0 || stride < row || end > header.cbSize { + return BOOL(1); + } + SINK.with(|slot| { + let slot = slot.borrow(); + let Some((sink, origin)) = slot.as_ref() else { + return; + }; + let mut pixels = Vec::with_capacity(row * header.height as usize); + for y in 0..header.height as usize { + // SAFETY: The magnifier guarantees `height` rows of `stride` bytes after `offset`, and + // each copied row stays within its stride. + pixels.extend_from_slice(unsafe { + std::slice::from_raw_parts( + source + .cast::() + .add(header.offset as usize + y * header.stride as usize), + row, + ) + }); + } + sink(PixelFrame { + pixels, + width: header.width, + height: header.height, + origin: *origin, + }); + }); + BOOL(1) +} + +fn run( + state: Arc, + ready: std::sync::mpsc::Sender>, + sink: Option, +) { + let callback = sink.is_some(); // SAFETY: Every handle below is created, used, and destroyed on this thread; the magnifier // runtime is initialized before the control is created and released after it is destroyed. unsafe { @@ -121,7 +197,11 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, class, w!("WinIsland Liquid Backdrop"), - WS_POPUP | WS_VISIBLE, + if callback { + WS_POPUP + } else { + WS_POPUP | WS_VISIBLE + }, source.left, source.top, (source.right - source.left).max(1), @@ -133,13 +213,15 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) ) .map_err(|error| format!("Magnifier host failed: {error}"))?; let _ = SetLayeredWindowAttributes(host, COLORREF(0), 255, LWA_ALPHA); - let cloak = 1_i32; - let _ = DwmSetWindowAttribute( - host, - DWMWA_CLOAK, - (&cloak as *const i32).cast(), - size_of::() as u32, - ); + if !callback { + let cloak = 1_i32; + let _ = DwmSetWindowAttribute( + host, + DWMWA_CLOAK, + (&cloak as *const i32).cast(), + size_of::() as u32, + ); + } let magnifier = CreateWindowExW( WINDOW_EX_STYLE(0), PCWSTR(WC_MAGNIFIERW.as_ptr()), @@ -173,6 +255,11 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) return; } }; + if let Some(sink) = sink { + let source = *state.source.lock(); + SINK.with(|slot| *slot.borrow_mut() = Some((sink, (source.left, source.top)))); + let _ = MagSetImageScalingCallback(magnifier, Some(on_frame)); + } let mut excluded = own_windows(); let mut filter_ok = apply_filter(magnifier, &excluded, true); let _ = ready.send(Ok(host.0 as isize)); @@ -214,6 +301,11 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) excluded = windows; filter_ok = apply_filter(magnifier, &excluded, filter_ok); } + SINK.with(|slot| { + if let Some((_, origin)) = slot.borrow_mut().as_mut() { + *origin = (source.left, source.top); + } + }); let _ = MagSetWindowSource(magnifier, source); let _ = InvalidateRect(Some(magnifier), None, false); while PeekMessageW(&mut message, None, 0, 0, PM_REMOVE).as_bool() { @@ -231,6 +323,7 @@ fn run(state: Arc, ready: std::sync::mpsc::Sender>) } let _ = DestroyWindow(host); let _ = MagUninitialize(); + SINK.with(|slot| slot.borrow_mut().take()); } }