fix(windows): hold one MTA for the process instead of init/uninit per call

Opening the share picker twice crashed the app with an access violation. The
capture paths wrapped each call in CoInitializeEx/CoUninitialize, but tearing
the apartment down does not invalidate the WinRT activation factories the
windows crate caches, so the second GraphicsCaptureSession::IsSupported read
freed memory. CoIncrementMTAUsage exists for this: an implicit MTA that outlives
every call, with the cookie deliberately never released. The worker threads stop
initialising their own apartments for the same reason.

Found by the capture self-test on the Windows CI job, which is why it exists.
This commit is contained in:
tonhowtf
2026-08-28 11:40:43 -03:00
parent 2fc399816e
commit 3fe2aed412
3 changed files with 21 additions and 34 deletions
@@ -18,9 +18,7 @@ use windows::Win32::Media::Audio::{
WAVEFORMATEX,
};
use windows::Win32::System::Com::StructuredStorage::{PROPVARIANT, PROPVARIANT_0_0};
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, BLOB, CLSCTX_ALL, COINIT_MULTITHREADED,
};
use windows::Win32::System::Com::{CoCreateInstance, CoIncrementMTAUsage, BLOB, CLSCTX_ALL};
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
use windows::Win32::System::Variant::VT_BLOB;
@@ -332,14 +330,11 @@ pub fn start_process_loopback(
let thread = std::thread::Builder::new()
.name("omnidisc-wasapi".into())
.spawn(move || {
let com = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
ensure_mta();
let ready = match Event::new() {
Ok(e) => e,
Err(e) => {
let _ = tx.send(Err(e));
if com.is_ok() {
unsafe { CoUninitialize() };
}
return;
}
};
@@ -354,9 +349,6 @@ pub fn start_process_loopback(
let _ = tx.send(Err(e));
}
}
if com.is_ok() {
unsafe { CoUninitialize() };
}
})
.map_err(|e| StreamError::Capture(format!("wasapi thread: {e}")))?;
match rx.recv() {
@@ -441,19 +433,21 @@ pub fn audio_apps() -> Vec<AudioApp> {
/// COM apartment guard for the enumeration helpers, which may run on a tokio
/// blocking thread whose apartment we do not control.
pub struct ComGuard(bool);
impl ComGuard {
pub fn mta() -> Self {
let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
Self(hr.is_ok())
}
}
impl Drop for ComGuard {
fn drop(&mut self) {
if self.0 {
unsafe { CoUninitialize() };
/// Guarantees the process has a multithreaded apartment, once, for good.
///
/// The obvious shape — initialise COM on entry, uninitialise on the way out —
/// crashes. `CoUninitialize` tears the apartment down, but the activation
/// factories the windows crate caches for WinRT types survive it, so the next
/// `GraphicsCaptureSession::IsSupported` dereferences freed memory: opening the
/// share picker a second time was enough to fault. `CoIncrementMTAUsage` is the
/// API for this exact need — it keeps an implicit MTA alive for every thread
/// that never initialises one itself, and the cookie is deliberately never
/// released.
pub fn ensure_mta() {
static MTA: std::sync::OnceLock<()> = std::sync::OnceLock::new();
MTA.get_or_init(|| {
if let Err(e) = unsafe { CoIncrementMTAUsage() } {
tracing::warn!("[omnidisc-media] could not hold an MTA open: {e}");
}
}
});
}
@@ -170,7 +170,7 @@ pub struct Platform;
impl CaptureApi for Platform {
fn list_sources(thumbnails: bool) -> Result<StreamSources, StreamError> {
let _com = audio::ComGuard::mta();
audio::ensure_mta();
sources::trace("com guard ready");
wgc_available()?;
sources::trace("wgc available");
@@ -209,7 +209,7 @@ impl CaptureApi for Platform {
// `GraphicsCaptureSession::IsSupported` is a WinRT static call, so the
// caller's thread needs an apartment even though the session itself
// runs on our own.
let _com = audio::ComGuard::mta();
audio::ensure_mta();
wgc_available()?;
let gpu = gpu()?;
let target = match opts.source {
@@ -17,7 +17,6 @@ use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct3D11::{ID3D11Texture2D, D3D11_BOX};
use windows::Win32::Graphics::Dxgi::IDXGIDevice;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED};
use windows::Win32::System::WinRT::Direct3D11::{
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
};
@@ -258,7 +257,7 @@ pub fn start(
let thread = std::thread::Builder::new()
.name("omnidisc-wgc".into())
.spawn(move || {
let com = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
super::audio::ensure_mta();
let last_frame_us = Arc::new(AtomicU64::new(0));
let started = capture_item(&target).and_then(|item| {
let size: SizeInt32 = item.Size().map_err(|e| cap_err("item size", e))?;
@@ -283,9 +282,6 @@ pub fn start(
}
Err(e) => {
let _ = tx.send(Err(e));
if com.is_ok() {
unsafe { CoUninitialize() };
}
return;
}
};
@@ -299,9 +295,6 @@ pub fn start(
}
let _ = session.session.Close();
let _ = session.pool.Close();
if com.is_ok() {
unsafe { CoUninitialize() };
}
})
.map_err(|e| StreamError::Capture(format!("wgc thread: {e}")))?;
match rx.recv() {