style(host/windows): rustfmt the Windows backends
apple / swift (push) Successful in 55s
android / android (push) Failing after 1m53s
ci / web (push) Failing after 17s
ci / docs-site (push) Successful in 42s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
ci / rust (push) Failing after 3m5s
ci / bench (push) Successful in 1m49s
decky / build-publish (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 0s
flatpak / build-publish (push) Failing after 0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
docker / deploy-docs (push) Has been skipped
deb / build-publish (push) Failing after 1m43s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m15s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 01:50:16 +00:00
parent 5cf7b561b5
commit 2448a33698
12 changed files with 177 additions and 93 deletions
+2 -1
View File
@@ -39,7 +39,8 @@ pub fn open_audio_capture(channels: u32) -> Result<Box<dyn AudioCapturer>> {
#[cfg(target_os = "windows")]
pub fn open_audio_capture(channels: u32) -> Result<Box<dyn AudioCapturer>> {
wasapi_cap::WasapiLoopbackCapturer::open(channels).map(|c| Box::new(c) as Box<dyn AudioCapturer>)
wasapi_cap::WasapiLoopbackCapturer::open(channels)
.map(|c| Box::new(c) as Box<dyn AudioCapturer>)
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
+14 -4
View File
@@ -46,7 +46,9 @@ impl WasapiLoopbackCapturer {
.context("spawn wasapi audio thread")?;
match ready_rx.recv_timeout(Duration::from_secs(3)) {
Ok(Ok(())) => {
tracing::info!("WASAPI loopback capture: 48 kHz stereo f32 (default render endpoint)");
tracing::info!(
"WASAPI loopback capture: 48 kHz stereo f32 (default render endpoint)"
);
Ok(WasapiLoopbackCapturer {
chunks: rx,
channels,
@@ -93,7 +95,10 @@ fn capture_thread(
ready: SyncSender<Result<()>>,
) -> Result<()> {
// COM must be initialized on THIS thread (MTA), before any device call.
if let Err(e) = wasapi::initialize_mta().ok().context("CoInitializeEx (MTA)") {
if let Err(e) = wasapi::initialize_mta()
.ok()
.context("CoInitializeEx (MTA)")
{
let _ = ready.send(Err(e));
return Ok(());
}
@@ -122,7 +127,9 @@ fn capture_thread(
let capture_client = audio_client
.get_audiocaptureclient()
.context("IAudioCaptureClient")?;
audio_client.start_stream().context("start loopback stream")?;
audio_client
.start_stream()
.context("start loopback stream")?;
let _ = ready.send(Ok(()));
let mut bytes: VecDeque<u8> = VecDeque::new();
@@ -182,7 +189,10 @@ mod tests {
};
assert_eq!(cap.channels(), 2);
match cap.next_chunk() {
Ok(samples) => assert!(samples.len() % 2 == 0, "interleaved stereo => even sample count"),
Ok(samples) => assert!(
samples.len() % 2 == 0,
"interleaved stereo => even sample count"
),
Err(e) => eprintln!("no audio within timeout (silent system?): {e:#}"),
}
}
+5 -3
View File
@@ -257,7 +257,9 @@ pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Bo
#[cfg(target_os = "windows")]
pub fn capture_virtual_output(vout: crate::vdisplay::VirtualOutput) -> Result<Box<dyn Capturer>> {
let target = vout.win_capture.clone().ok_or_else(|| {
anyhow::anyhow!("SudoVDA target not yet an active display (needs a WDDM GPU to activate it)")
anyhow::anyhow!(
"SudoVDA target not yet an active display (needs a WDDM GPU to activate it)"
)
})?;
dxgi::DuplCapturer::open(target, vout.preferred_mode, vout.keepalive)
.map(|c| Box::new(c) as Box<dyn Capturer>)
@@ -268,7 +270,7 @@ pub fn capture_virtual_output(_vout: crate::vdisplay::VirtualOutput) -> Result<B
anyhow::bail!("virtual-output capture requires Linux or Windows")
}
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "windows")]
pub mod dxgi;
#[cfg(target_os = "linux")]
mod linux;
+11 -4
View File
@@ -139,8 +139,8 @@ impl DuplCapturer {
}
j += 1;
}
let output = out1
.with_context(|| format!("adapter has no output named {}", target.gdi_name))?;
let output =
out1.with_context(|| format!("adapter has no output named {}", target.gdi_name))?;
// 4) duplicate the output.
let dupl = output
.DuplicateOutput(&device)
@@ -171,7 +171,11 @@ impl DuplCapturer {
height,
refresh_hz,
target.gdi_name,
if gpu_mode { "D3D11 zero-copy" } else { "CPU staging" }
if gpu_mode {
"D3D11 zero-copy"
} else {
"CPU staging"
}
);
Ok(Self {
device,
@@ -268,7 +272,10 @@ impl DuplCapturer {
}
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut res: Option<IDXGIResource> = None;
match self.dupl.AcquireNextFrame(self.timeout_ms, &mut info, &mut res) {
match self
.dupl
.AcquireNextFrame(self.timeout_ms, &mut info, &mut res)
{
Ok(()) => {}
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => return Ok(None),
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
+9 -4
View File
@@ -185,8 +185,13 @@ pub fn open_video(
);
// Software H.264 realistically caps far below the negotiated hardware rates.
const SW_BITRATE_CEIL: u64 = 100_000_000;
let enc =
sw::OpenH264Encoder::open(format, width, height, fps, bitrate_bps.min(SW_BITRATE_CEIL))?;
let enc = sw::OpenH264Encoder::open(
format,
width,
height,
fps,
bitrate_bps.min(SW_BITRATE_CEIL),
)?;
Ok(Box::new(enc) as Box<dyn Encoder>)
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
@@ -198,10 +203,10 @@ pub fn open_video(
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "windows")]
mod sw;
#[cfg(all(target_os = "windows", feature = "nvenc"))]
mod nvenc;
#[cfg(target_os = "windows")]
mod sw;
#[cfg(test)]
mod tests {
+7 -3
View File
@@ -18,8 +18,8 @@ use std::ffi::c_void;
use std::ptr;
use windows::core::Interface;
use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
@@ -94,7 +94,11 @@ impl NvencD3d11Encoder {
/// Lazily create the session on the first frame's D3D11 device (so capture + encode share it).
fn init_session(&mut self, device: &ID3D11Device) -> Result<()> {
unsafe {
self.ctx = Some(device.GetImmediateContext().context("D3D11 immediate context")?);
self.ctx = Some(
device
.GetImmediateContext()
.context("D3D11 immediate context")?,
);
// 1. open the session bound to the D3D11 device.
let mut params = nv::NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
+6 -4
View File
@@ -132,10 +132,12 @@ impl Encoder for OpenH264Encoder {
);
match self.src_format {
PixelFormat::Rgb => self.yuv.read_rgb(RgbSliceU8::new(&bytes[..w * h * 3], (w, h))),
PixelFormat::Bgra | PixelFormat::Bgrx => {
self.yuv.read_rgb(BgraSliceU8::new(&bytes[..w * h * 4], (w, h)))
}
PixelFormat::Rgb => self
.yuv
.read_rgb(RgbSliceU8::new(&bytes[..w * h * 3], (w, h))),
PixelFormat::Bgra | PixelFormat::Bgrx => self
.yuv
.read_rgb(BgraSliceU8::new(&bytes[..w * h * 4], (w, h))),
PixelFormat::Rgba | PixelFormat::Rgbx => {
self.normalize_to_bgra(bytes, 4, true);
self.yuv.read_rgb(BgraSliceU8::new(&self.scratch, (w, h)));
+2 -2
View File
@@ -317,7 +317,7 @@ pub mod gamepad {
}
#[cfg(target_os = "linux")]
mod libei;
#[cfg(target_os = "linux")]
mod wlr;
#[cfg(target_os = "windows")]
mod sendinput;
#[cfg(target_os = "linux")]
mod wlr;
+58 -13
View File
@@ -13,11 +13,11 @@ use windows::Win32::System::StationsAndDesktops::{
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
MapVirtualKeyExW, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT,
KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, MAPVK_VK_TO_VSC_EX, MOUSEEVENTF_ABSOLUTE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT,
VIRTUAL_KEY,
KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, MAPVK_VK_TO_VSC_EX,
MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN,
MOUSEEVENTF_XUP, MOUSEINPUT, VIRTUAL_KEY,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
@@ -73,7 +73,10 @@ impl SendInputInjector {
if n as usize != inputs.len() {
// 0 = blocked (different/secure desktop). Surface as Err so the host service drops +
// reopens the injector (which reattaches the input desktop).
anyhow::bail!("SendInput injected {n}/{} events (blocked desktop?)", inputs.len());
anyhow::bail!(
"SendInput injected {n}/{} events (blocked desktop?)",
inputs.len()
);
}
Ok(())
}
@@ -130,11 +133,46 @@ impl InputInjector for SendInputInjector {
InputKind::MouseButtonDown | InputKind::MouseButtonUp => {
let down = event.kind == InputKind::MouseButtonDown;
let (flag, data) = match event.code {
1 => (if down { MOUSEEVENTF_LEFTDOWN } else { MOUSEEVENTF_LEFTUP }, 0u32),
2 => (if down { MOUSEEVENTF_MIDDLEDOWN } else { MOUSEEVENTF_MIDDLEUP }, 0),
3 => (if down { MOUSEEVENTF_RIGHTDOWN } else { MOUSEEVENTF_RIGHTUP }, 0),
4 => (if down { MOUSEEVENTF_XDOWN } else { MOUSEEVENTF_XUP }, XBUTTON1),
5 => (if down { MOUSEEVENTF_XDOWN } else { MOUSEEVENTF_XUP }, XBUTTON2),
1 => (
if down {
MOUSEEVENTF_LEFTDOWN
} else {
MOUSEEVENTF_LEFTUP
},
0u32,
),
2 => (
if down {
MOUSEEVENTF_MIDDLEDOWN
} else {
MOUSEEVENTF_MIDDLEUP
},
0,
),
3 => (
if down {
MOUSEEVENTF_RIGHTDOWN
} else {
MOUSEEVENTF_RIGHTUP
},
0,
),
4 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON1,
),
5 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON2,
),
_ => return Ok(()),
};
let mi = MOUSEINPUT {
@@ -155,7 +193,11 @@ impl InputInjector for SendInputInjector {
dx: 0,
dy: 0,
mouseData: event.x as u32, // signed wheel delta reinterpreted as DWORD
dwFlags: if horizontal { MOUSEEVENTF_HWHEEL } else { MOUSEEVENTF_WHEEL },
dwFlags: if horizontal {
MOUSEEVENTF_HWHEEL
} else {
MOUSEEVENTF_WHEEL
},
time: 0,
dwExtraInfo: 0,
};
@@ -226,5 +268,8 @@ fn virtual_desktop_rect() -> (i32, i32, i32, i32) {
// RCtrl (0xA3), RAlt (0xA5), Pause (0x90). MAPVK_VK_TO_VSC_EX already encodes E0 for most; this is a
// thin safety net.
fn forced_extended(vk: u16) -> bool {
matches!(vk, 0x21..=0x28 | 0x2D | 0x2E | 0x5B | 0x5C | 0x5D | 0xA3 | 0xA5 | 0x90)
matches!(
vk,
0x21..=0x28 | 0x2D | 0x2E | 0x5B | 0x5C | 0x5D | 0xA3 | 0xA5 | 0x90
)
}
+43 -43
View File
@@ -1528,52 +1528,52 @@ fn resolve_compositor(pref: CompositorPref) -> Result<crate::vdisplay::Composito
}
#[cfg(not(target_os = "windows"))]
{
// Explicit operator override (legacy / CI / forcing a backend for a test) wins and is assumed
// to come with a hand-set env — don't retarget the process env in that case.
let overridden = std::env::var_os("PUNKTFUNK_COMPOSITOR").is_some();
let detected = if overridden {
crate::vdisplay::detect().ok()
} else {
// Auto: detect the LIVE session (Gaming vs Desktop) and retarget the process env at it so
// every backend (video capture + input) this connect opens against the active session —
// this is the state machine that lets one host follow a Bazzite box across Gaming↔Desktop.
let active = crate::vdisplay::detect_active_session();
crate::vdisplay::apply_session_env(&active);
tracing::info!(
active = ?active.kind,
wayland = active.env.wayland_display.as_deref().unwrap_or("-"),
"detected active graphical session"
);
crate::vdisplay::compositor_for_kind(active.kind)
};
let available = crate::vdisplay::available();
let chosen = pick_compositor(pref, &available, detected).ok_or_else(|| {
// Explicit operator override (legacy / CI / forcing a backend for a test) wins and is assumed
// to come with a hand-set env — don't retarget the process env in that case.
let overridden = std::env::var_os("PUNKTFUNK_COMPOSITOR").is_some();
let detected = if overridden {
crate::vdisplay::detect().ok()
} else {
// Auto: detect the LIVE session (Gaming vs Desktop) and retarget the process env at it so
// every backend (video capture + input) this connect opens against the active session —
// this is the state machine that lets one host follow a Bazzite box across Gaming↔Desktop.
let active = crate::vdisplay::detect_active_session();
crate::vdisplay::apply_session_env(&active);
tracing::info!(
active = ?active.kind,
wayland = active.env.wayland_display.as_deref().unwrap_or("-"),
"detected active graphical session"
);
crate::vdisplay::compositor_for_kind(active.kind)
};
let available = crate::vdisplay::available();
let chosen = pick_compositor(pref, &available, detected).ok_or_else(|| {
anyhow!("no usable compositor (no live graphical session for this uid; set PUNKTFUNK_COMPOSITOR or start a desktop/gaming session)")
})?;
if !overridden {
// Point input at the same backend and select gamescope ATTACH (no churny managed restart).
crate::vdisplay::apply_input_env(chosen);
}
let avail_ids: Vec<&str> = available.iter().map(|c| c.id()).collect();
match Compositor::from_pref(pref) {
Some(want) if want == chosen => {
tracing::info!(
compositor = chosen.id(),
"honoring client compositor request"
)
if !overridden {
// Point input at the same backend and select gamescope ATTACH (no churny managed restart).
crate::vdisplay::apply_input_env(chosen);
}
Some(want) => tracing::warn!(
requested = want.id(),
chosen = chosen.id(),
available = ?avail_ids,
"client-requested compositor unavailable — falling back to auto-detect"
),
None => tracing::info!(
compositor = chosen.id(),
"auto-detected compositor (client: auto)"
),
}
Ok(chosen)
let avail_ids: Vec<&str> = available.iter().map(|c| c.id()).collect();
match Compositor::from_pref(pref) {
Some(want) if want == chosen => {
tracing::info!(
compositor = chosen.id(),
"honoring client compositor request"
)
}
Some(want) => tracing::warn!(
requested = want.id(),
chosen = chosen.id(),
available = ?avail_ids,
"client-requested compositor unavailable — falling back to auto-detect"
),
None => tracing::info!(
compositor = chosen.id(),
"auto-detected compositor (client: auto)"
),
}
Ok(chosen)
}
}
+2 -2
View File
@@ -540,10 +540,10 @@ mod gamescope;
mod kwin;
#[cfg(target_os = "linux")]
mod mutter;
#[cfg(target_os = "linux")]
mod wlroots;
#[cfg(target_os = "windows")]
mod sudovda;
#[cfg(target_os = "linux")]
mod wlroots;
#[cfg(test)]
mod tests {
+18 -10
View File
@@ -229,11 +229,16 @@ impl VirtualDisplay for SudoVdaDisplay {
device_name,
serial: [0u8; 14],
};
let add_bytes =
unsafe { std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::<AddParams>()) };
let add_bytes = unsafe {
std::slice::from_raw_parts(&add as *const _ as *const u8, size_of::<AddParams>())
};
let mut out = [0u8; size_of::<AddOut>()];
unsafe { ioctl(self.device, IOCTL_ADD, add_bytes, &mut out) }
.with_context(|| format!("SudoVDA ADD {}x{}@{}", mode.width, mode.height, mode.refresh_hz))?;
unsafe { ioctl(self.device, IOCTL_ADD, add_bytes, &mut out) }.with_context(|| {
format!(
"SudoVDA ADD {}x{}@{}",
mode.width, mode.height, mode.refresh_hz
)
})?;
let ao = unsafe { *(out.as_ptr() as *const AddOut) };
tracing::info!(
"SudoVDA created {}x{}@{} (target_id={}, adapter_luid={:#x})",
@@ -281,10 +286,12 @@ impl VirtualDisplay for SudoVdaDisplay {
Ok(VirtualOutput {
node_id: 0, // unused on Windows; the capture target is the GDI name below
preferred_mode: Some((mode.width, mode.height, mode.refresh_hz)),
win_capture: gdi_name.clone().map(|n| crate::capture::dxgi::WinCaptureTarget {
adapter_luid: crate::capture::dxgi::pack_luid(ao.luid),
gdi_name: n,
}),
win_capture: gdi_name
.clone()
.map(|n| crate::capture::dxgi::WinCaptureTarget {
adapter_luid: crate::capture::dxgi::pack_luid(ao.luid),
gdi_name: n,
}),
keepalive: Box::new(SudoVdaKeepalive {
device: device_raw,
guid: MONITOR_GUID,
@@ -314,8 +321,9 @@ impl Drop for SudoVdaKeepalive {
let _ = j.join();
}
let rp = RemoveParams { guid: self.guid };
let rp_bytes =
unsafe { std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::<RemoveParams>()) };
let rp_bytes = unsafe {
std::slice::from_raw_parts(&rp as *const _ as *const u8, size_of::<RemoveParams>())
};
let mut none: [u8; 0] = [];
let h = HANDLE(self.device as *mut c_void);
if let Err(e) = unsafe { ioctl(h, IOCTL_REMOVE, rp_bytes, &mut none) } {