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