chore(repo): prune dead code and duplicate abstractions (#3413)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-12 13:09:42 +02:00
1 parent c17091768c
commit c9fdf8fead
2937 files changed
+42233 -176793

No files matched your search

@@ -1,201 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::ptr::{null, null_mut};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, RECT},
Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO,
MonitorFromRect,
},
System::Threading::GetCurrentProcessId,
UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetForegroundWindow, GetWindow, GetWindowLongPtrW,
GetWindowRect, GetWindowThreadProcessId, IsWindow, IsWindowVisible, WS_BORDER, WS_MAXIMIZE,
},
};
const MONITOR_LIMIT: usize = 16;
fn parse_hwnd_source_id(source_id: &str) -> Option<HWND> {
let token = source_id.strip_prefix("window:")?.split(':').next()?;
let value = if let Some(hex) = token
.strip_prefix("0x")
.or_else(|| token.strip_prefix("0X"))
{
isize::from_str_radix(hex, 16).ok()?
} else {
token.parse::<isize>().ok()?
};
let hwnd = value as HWND;
(unsafe { IsWindow(hwnd) } != 0).then_some(hwnd)
}
fn parse_screen_ordinal(source_id: &str) -> Option<usize> {
let token = source_id.strip_prefix("screen:")?.split(':').next()?;
token.parse::<usize>().ok()
}
pub(crate) fn resolve_game_capture_target(
source_id: &str,
source_kind: &str,
) -> Result<HWND, String> {
if let Some(hwnd) = parse_hwnd_source_id(source_id) {
return Ok(hwnd);
}
if source_kind != "game" && source_kind != "screen" {
return Err(format!(
"invalid game capture source: {source_kind}:{source_id}"
));
}
let monitor = monitor_for_screen_source(source_id)?;
find_fullscreen_window_on_monitor(monitor)
.or_else(find_foreground_fullscreen_window)
.or_else(find_fullscreen_window_on_any_monitor)
.ok_or_else(|| "no fullscreen game window found on selected display".to_string())
}
fn monitor_for_screen_source(source_id: &str) -> Result<HMONITOR, String> {
let ordinal = parse_screen_ordinal(source_id).unwrap_or(0);
let monitors = enumerate_monitors();
monitors
.get(ordinal)
.copied()
.or_else(|| monitors.first().copied())
.ok_or_else(|| "no monitors available for game capture".to_string())
}
fn enumerate_monitors() -> Vec<HMONITOR> {
unsafe extern "system" fn enum_monitor(
monitor: HMONITOR,
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
_rect: *mut RECT,
param: LPARAM,
) -> i32 {
let monitors = &mut *(param as *mut Vec<HMONITOR>);
if monitors.len() >= MONITOR_LIMIT {
return 0;
}
monitors.push(monitor);
1
}
let mut monitors = Vec::with_capacity(MONITOR_LIMIT);
unsafe {
EnumDisplayMonitors(
null_mut(),
null(),
Some(enum_monitor),
&mut monitors as *mut _ as LPARAM,
);
}
assert!(monitors.len() <= MONITOR_LIMIT, "monitor targets bounded");
monitors
}
pub(crate) fn monitor_rect(monitor: HMONITOR) -> Option<RECT> {
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
rcMonitor: RECT::default(),
rcWork: RECT::default(),
dwFlags: 0,
};
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
return None;
}
Some(info.rcMonitor)
}
fn rect_matches_monitor(window_rect: &RECT, monitor_rect: &RECT) -> bool {
const TOLERANCE: i32 = 2;
(window_rect.left - monitor_rect.left).abs() <= TOLERANCE
&& (window_rect.top - monitor_rect.top).abs() <= TOLERANCE
&& (window_rect.right - monitor_rect.right).abs() <= TOLERANCE
&& (window_rect.bottom - monitor_rect.bottom).abs() <= TOLERANCE
}
fn is_regular_maximized_window(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
(style & WS_MAXIMIZE) != 0 && (style & WS_BORDER) != 0
}
fn is_fullscreen_window_on_monitor(hwnd: HWND, monitor: HMONITOR, monitor_rect: &RECT) -> bool {
if unsafe { IsWindowVisible(hwnd) } == 0 {
return false;
}
if !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
return false;
}
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
}
if pid == 0 || pid == unsafe { GetCurrentProcessId() } {
return false;
}
if is_regular_maximized_window(hwnd) {
return false;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return false;
}
let window_monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
window_monitor == monitor && rect_matches_monitor(&rect, monitor_rect)
}
fn find_foreground_fullscreen_window() -> Option<HWND> {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.is_null() {
return None;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return None;
}
let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
let monitor_rect = monitor_rect(monitor)?;
is_fullscreen_window_on_monitor(hwnd, monitor, &monitor_rect).then_some(hwnd)
}
fn find_fullscreen_window_on_any_monitor() -> Option<HWND> {
for monitor in enumerate_monitors() {
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
return Some(hwnd);
}
}
None
}
fn find_fullscreen_window_on_monitor(monitor: HMONITOR) -> Option<HWND> {
struct Search {
monitor: HMONITOR,
monitor_rect: RECT,
result: HWND,
own_pid: u32,
}
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
let search = &mut *(param as *mut Search);
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, &mut pid);
if pid == 0 || pid == search.own_pid {
return 1;
}
if !is_fullscreen_window_on_monitor(hwnd, search.monitor, &search.monitor_rect) {
return 1;
}
search.result = hwnd;
0
}
let monitor_rect = monitor_rect(monitor)?;
let mut search = Search {
monitor,
monitor_rect,
result: null_mut(),
own_pid: unsafe { GetCurrentProcessId() },
};
unsafe {
let _ = EnumWindows(Some(enum_window), &mut search as *mut _ as LPARAM);
}
(!search.result.is_null()).then_some(search.result)
}
@@ -1,46 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use windows::Win32::{
Foundation::HMODULE,
Graphics::{
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device,
ID3D11DeviceContext,
},
Dxgi::{IDXGIAdapter, IDXGIDevice},
},
};
use windows::core::Interface;
pub(crate) fn create_shared_texture_device(
adapter: Option<&IDXGIAdapter>,
) -> Result<(ID3D11Device, ID3D11DeviceContext), String> {
let mut device = None;
let mut context = None;
let driver_type = if adapter.is_some() {
D3D_DRIVER_TYPE_UNKNOWN
} else {
D3D_DRIVER_TYPE_HARDWARE
};
unsafe {
D3D11CreateDevice(
adapter,
driver_type,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
.map_err(|error| format!("D3D11CreateDevice for shared capture texture: {error}"))?;
}
let device = device.ok_or("D3D11 shared texture device was None")?;
let context = context.ok_or("D3D11 shared texture context was None")?;
if let Ok(dxgi_device) = device.cast::<IDXGIDevice>() {
let _ = unsafe { dxgi_device.SetGPUThreadPriority(7) };
}
Ok((device, context))
}
File diff suppressed because it is too large. Load diff
@@ -1,417 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use parking_lot::{Condvar, Mutex};
use fluxer_encoder_ring::d3d11::D3D11SharedHandle;
use fluxer_encoder_ring::{
D3D11KeyedMutexBackend, EncoderFrameRate, EncoderInputRing, EncoderReady, RING_SIZE, RingError,
TextureFormat,
};
#[cfg(target_os = "windows")]
use fluxer_encoder_ring::{EncoderDims, FillReservation, NvencD3D11Handoff};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct3D11::{ID3D11DeviceContext, ID3D11Texture2D};
pub const READY_WAIT_TIMEOUT_CAP: Duration = Duration::from_millis(17);
const EVICTIONS_PER_RESERVE_MAX: u64 = 2;
#[cfg(target_os = "windows")]
const NVENC_BITRATE_BPS_DEFAULT: u32 = 8_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderAttachError {
AlreadyAttached,
InvalidDimensions { width: u32, height: u32 },
RingInitFailed,
NotAttached,
BlitFailed,
SlotUnavailable,
DeviceUnavailable,
}
impl std::fmt::Display for EncoderAttachError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyAttached => write!(f, "encoder ring already attached"),
Self::InvalidDimensions { width, height } => {
write!(f, "encoder ring invalid dimensions {width}x{height}")
}
Self::RingInitFailed => write!(f, "encoder ring initialise failed"),
Self::NotAttached => write!(f, "encoder ring not attached"),
Self::BlitFailed => write!(f, "blit into ring slot failed"),
Self::SlotUnavailable => write!(f, "no ring slot texture available"),
Self::DeviceUnavailable => write!(f, "ring backend device unavailable"),
}
}
}
impl std::error::Error for EncoderAttachError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EncoderAttachStats {
pub frames_submitted: u64,
pub frames_dropped: u64,
pub ring_full_events: u64,
pub failed_blits: u64,
}
pub struct EncoderAttachment {
ring: Mutex<EncoderInputRing<D3D11KeyedMutexBackend>>,
ready_condvar: Condvar,
attached: AtomicBool,
frames_submitted: AtomicU64,
frames_dropped: AtomicU64,
ring_full_events: AtomicU64,
failed_blits: AtomicU64,
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
#[cfg(target_os = "windows")]
context: ID3D11DeviceContext,
#[cfg(target_os = "windows")]
nvenc: Mutex<Option<NvencD3D11Handoff>>,
}
impl EncoderAttachment {
pub fn try_new(width: u32, height: u32) -> Result<Arc<Self>, EncoderAttachError> {
Self::try_new_with_frame_rate(width, height, EncoderFrameRate::default())
}
pub fn try_new_with_frame_rate(
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
) -> Result<Arc<Self>, EncoderAttachError> {
if width == 0 || height == 0 {
return Err(EncoderAttachError::InvalidDimensions { width, height });
}
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
assert!(
frame_rate.denominator > 0,
"frame rate denominator positive"
);
let backend =
D3D11KeyedMutexBackend::new().map_err(|_| EncoderAttachError::RingInitFailed)?;
let mut ring = EncoderInputRing::new(backend);
ring.initialise(width, height, TextureFormat::Nv12)
.map_err(|_| EncoderAttachError::RingInitFailed)?;
#[cfg(target_os = "windows")]
let context = ring
.backend_mut()
.context()
.ok_or(EncoderAttachError::DeviceUnavailable)?;
#[cfg(target_os = "windows")]
let nvenc = init_nvenc_pre_registered(&mut ring, width, height, frame_rate);
let attachment = Self {
ring: Mutex::new(ring),
ready_condvar: Condvar::new(),
attached: AtomicBool::new(true),
frames_submitted: AtomicU64::new(0),
frames_dropped: AtomicU64::new(0),
ring_full_events: AtomicU64::new(0),
failed_blits: AtomicU64::new(0),
width,
height,
frame_rate,
#[cfg(target_os = "windows")]
context,
#[cfg(target_os = "windows")]
nvenc: Mutex::new(nvenc),
};
assert!(
attachment.attached.load(Ordering::Acquire),
"attachment is attached"
);
assert!(attachment.width > 0, "attachment width positive");
assert!(
attachment.frame_rate.numerator > 0,
"attachment fps positive"
);
Ok(Arc::new(attachment))
}
pub fn width(&self) -> u32 {
let w = self.width;
assert!(w > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
w
}
pub fn height(&self) -> u32 {
let h = self.height;
assert!(h > 0, "attachment height positive");
assert!(self.width > 0, "attachment width positive");
h
}
pub fn frame_rate(&self) -> EncoderFrameRate {
let rate = self.frame_rate;
assert!(rate.numerator > 0, "attachment fps numerator positive");
assert!(rate.denominator > 0, "attachment fps denominator positive");
rate
}
pub fn is_attached(&self) -> bool {
let a = self.attached.load(Ordering::Acquire);
assert!(self.width > 0, "width intact while reading attached");
assert!(self.height > 0, "height intact while reading attached");
a
}
pub fn detach(&self) {
self.attached.store(false, Ordering::Release);
self.ready_condvar.notify_all();
assert!(
!self.attached.load(Ordering::Acquire),
"detach flag cleared"
);
assert!(self.width > 0, "attachment intact across detach");
}
pub fn capacity(&self) -> usize {
let cap = RING_SIZE;
assert!(cap > 0, "ring capacity positive");
assert_eq!(cap, 8, "ring capacity matches RING_SIZE");
cap
}
pub fn note_ring_full(&self) {
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
}
#[cfg(target_os = "windows")]
pub fn nvenc_pre_registered(&self) -> bool {
let registered = self.nvenc.lock().is_some();
assert!(self.width > 0, "width intact while reading nvenc state");
assert!(self.height > 0, "height intact while reading nvenc state");
registered
}
pub fn stats(&self) -> EncoderAttachStats {
let stats = EncoderAttachStats {
frames_submitted: self.frames_submitted.load(Ordering::Relaxed),
frames_dropped: self.frames_dropped.load(Ordering::Relaxed),
ring_full_events: self.ring_full_events.load(Ordering::Relaxed),
failed_blits: self.failed_blits.load(Ordering::Relaxed),
};
assert!(
stats.frames_submitted <= u64::MAX / 2,
"submitted plausible"
);
assert!(stats.frames_dropped <= u64::MAX / 2, "dropped plausible");
stats
}
fn account_evictions(&self, evicted_delta: u64) {
assert!(
evicted_delta <= EVICTIONS_PER_RESERVE_MAX,
"evictions bounded per reserve"
);
assert!(self.width > 0, "attachment intact while accounting");
if evicted_delta == 0 {
return;
}
self.frames_dropped
.fetch_add(evicted_delta, Ordering::Relaxed);
self.ring_full_events
.fetch_add(evicted_delta, Ordering::Relaxed);
}
pub fn submit_notify(&self) -> Result<(), EncoderAttachError> {
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let mut ring = self.ring.lock();
let pre_dropped = ring.dropped_count();
let result: Result<(), RingError> = ring.submit_skip_oldest(|_handle| {});
let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
drop(ring);
self.account_evictions(evicted_delta);
match result {
Ok(()) => {
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
Err(RingError::FullDropped { .. }) => Ok(()),
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
pub fn wait_next_ready(&self, timeout: Duration) -> Option<EncoderReady<D3D11SharedHandle>> {
assert!(self.width > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
if !self.attached.load(Ordering::Acquire) {
return None;
}
let capped_timeout = timeout.min(READY_WAIT_TIMEOUT_CAP);
let mut ring = self.ring.lock();
if let Some(ready) = ring.poll_next_ready() {
return Some(ready);
}
let _ = self.ready_condvar.wait_for(&mut ring, capped_timeout);
if !self.attached.load(Ordering::Acquire) {
return None;
}
ring.poll_next_ready()
}
#[cfg(target_os = "windows")]
fn cancel_reservation(&self, reservation: FillReservation<D3D11SharedHandle>) {
assert!(
(reservation.slot_index() as usize) < RING_SIZE,
"cancelled slot within ring"
);
assert!(self.width > 0, "attachment intact while cancelling");
let mut ring = self.ring.lock();
let _ = ring.cancel(reservation);
}
#[cfg(target_os = "windows")]
pub fn submit_capture_frame_with_blit(
&self,
capture_texture: &ID3D11Texture2D,
capture_width: u32,
capture_height: u32,
) -> Result<(), EncoderAttachError> {
assert!(capture_width > 0, "capture width positive");
assert!(capture_height > 0, "capture height positive");
assert!(self.width > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let mut ring = self.ring.lock();
let pre_dropped = ring.dropped_count();
let reserved = ring.reserve_skip_oldest();
let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
let dest_texture = match &reserved {
Ok(reservation) => ring
.backend_mut()
.texture_for_slot(reservation.slot_index()),
Err(_) => None,
};
drop(ring);
self.account_evictions(evicted_delta);
let reservation = match reserved {
Ok(reservation) => reservation,
Err(RingError::FullDropped { .. }) => return Ok(()),
Err(_) => return Err(EncoderAttachError::RingInitFailed),
};
let Some(dest_texture) = dest_texture else {
self.cancel_reservation(reservation);
self.failed_blits.fetch_add(1, Ordering::Relaxed);
return Err(EncoderAttachError::BlitFailed);
};
unsafe {
self.context.CopyResource(&dest_texture, capture_texture);
}
let mut ring = self.ring.lock();
match ring.commit(reservation) {
Ok(sequence) => {
drop(ring);
assert!(sequence > 0, "committed sequence positive");
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
}
#[cfg(target_os = "windows")]
fn collect_ring_slot_handles(
ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
) -> Option<Vec<D3D11SharedHandle>> {
assert_eq!(
ring.free_count(),
RING_SIZE,
"collect requires a fresh ring"
);
let mut reservations: Vec<FillReservation<D3D11SharedHandle>> = Vec::with_capacity(RING_SIZE);
let mut handles: Vec<D3D11SharedHandle> = Vec::with_capacity(RING_SIZE);
for _ in 0..RING_SIZE {
let Ok(reservation) = ring.reserve() else {
break;
};
handles.push(reservation.handle.clone());
reservations.push(reservation);
}
let mut all_cancelled = true;
for reservation in reservations.drain(..) {
if ring.cancel(reservation).is_err() {
all_cancelled = false;
}
}
if !all_cancelled {
return None;
}
assert_eq!(ring.free_count(), RING_SIZE, "all reservations returned");
assert!(handles.len() <= RING_SIZE, "collected handles bounded");
if handles.len() == RING_SIZE {
Some(handles)
} else {
None
}
}
#[cfg(target_os = "windows")]
fn init_nvenc_pre_registered(
ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
) -> Option<NvencD3D11Handoff> {
assert!(width > 0, "nvenc init width positive");
assert!(height > 0, "nvenc init height positive");
assert!(frame_rate.numerator > 0, "nvenc frame rate positive");
let device = ring.backend_mut().device()?;
let handles = collect_ring_slot_handles(ring)?;
assert_eq!(handles.len(), RING_SIZE, "pre-register covers whole ring");
let dims = EncoderDims::new(width, height);
let mut handoff =
NvencD3D11Handoff::new_with_frame_rate(device, dims, NVENC_BITRATE_BPS_DEFAULT, frame_rate)
.ok()?;
handoff.pre_register_slots(&handles, dims).ok()?;
Some(handoff)
}
impl Drop for EncoderAttachment {
fn drop(&mut self) {
self.attached.store(false, Ordering::Release);
self.ready_condvar.notify_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_zero_dimensions() {
let err = EncoderAttachment::try_new(0, 720).err();
assert!(matches!(
err,
Some(EncoderAttachError::InvalidDimensions { .. })
));
let err2 = EncoderAttachment::try_new(640, 0).err();
assert!(matches!(
err2,
Some(EncoderAttachError::InvalidDimensions { .. })
));
}
#[test]
fn ready_wait_timeout_cap_is_one_frame_interval() {
assert!(READY_WAIT_TIMEOUT_CAP >= Duration::from_millis(1));
assert!(READY_WAIT_TIMEOUT_CAP <= Duration::from_millis(33));
}
}
@@ -1,710 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
use crate::game_capture_abi::{
GAME_CAPTURE_FALLBACK_DEVICE_LOST, GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_FORCED_CPU, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_MULTISAMPLED, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CaptureStrategy {
GameHook,
Wgc,
DxgiDuplication,
WindowGdi,
}
impl CaptureStrategy {
pub const RANKED: [CaptureStrategy; 4] = [
CaptureStrategy::GameHook,
CaptureStrategy::Wgc,
CaptureStrategy::DxgiDuplication,
CaptureStrategy::WindowGdi,
];
pub fn rank(self) -> u8 {
match self {
CaptureStrategy::GameHook => 0,
CaptureStrategy::Wgc => 1,
CaptureStrategy::DxgiDuplication => 2,
CaptureStrategy::WindowGdi => 3,
}
}
pub fn next_worse(self) -> Option<CaptureStrategy> {
match self {
CaptureStrategy::GameHook => Some(CaptureStrategy::Wgc),
CaptureStrategy::Wgc => Some(CaptureStrategy::DxgiDuplication),
CaptureStrategy::DxgiDuplication => Some(CaptureStrategy::WindowGdi),
CaptureStrategy::WindowGdi => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
CaptureStrategy::GameHook => "game-hook",
CaptureStrategy::Wgc => "wgc",
CaptureStrategy::DxgiDuplication => "dxgi-duplication",
CaptureStrategy::WindowGdi => "window-gdi",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FailureSignature {
InjectionFailed,
AntiCheatDenied,
NoFramesWithinTimeout,
DeviceLost,
UnsupportedTransport,
UnsupportedFormat,
WindowGone,
FramesRecovered,
}
impl FailureSignature {
pub fn from_fallback_reason(reason: u32) -> Option<FailureSignature> {
match reason {
GAME_CAPTURE_FALLBACK_NONE => None,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED
| GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED => {
Some(FailureSignature::UnsupportedTransport)
}
GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED | GAME_CAPTURE_FALLBACK_MULTISAMPLED => {
Some(FailureSignature::UnsupportedFormat)
}
GAME_CAPTURE_FALLBACK_DEVICE_LOST => Some(FailureSignature::DeviceLost),
GAME_CAPTURE_FALLBACK_FORCED_CPU => None,
_ => None,
}
}
fn is_terminal_for_all(self) -> bool {
matches!(self, FailureSignature::WindowGone)
}
fn permanently_disqualifies_current(self) -> bool {
matches!(
self,
FailureSignature::AntiCheatDenied
| FailureSignature::InjectionFailed
| FailureSignature::UnsupportedTransport
| FailureSignature::UnsupportedFormat
)
}
}
pub const MAX_DEVICE_LOST_RETRIES: u32 = 3;
pub const STABLE_FRAMES_BEFORE_UPGRADE: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackDecision {
Stay {
retry_in_place: bool,
},
Transition {
from: CaptureStrategy,
to: CaptureStrategy,
reason: String,
},
Upgrade {
from: CaptureStrategy,
to: CaptureStrategy,
reason: String,
},
GiveUp {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct FallbackRanker {
active: CaptureStrategy,
disqualified: Vec<CaptureStrategy>,
device_lost_streak: u32,
stable_frame_streak: u32,
last_fallback_reason: Option<String>,
}
impl FallbackRanker {
pub fn new(initial: CaptureStrategy) -> Self {
Self {
active: initial,
disqualified: Vec::new(),
device_lost_streak: 0,
stable_frame_streak: 0,
last_fallback_reason: None,
}
}
pub fn active(&self) -> CaptureStrategy {
self.active
}
pub fn last_fallback_reason(&self) -> Option<&str> {
self.last_fallback_reason.as_deref()
}
pub fn is_disqualified(&self, strategy: CaptureStrategy) -> bool {
self.disqualified.contains(&strategy)
}
fn best_available_above(&self) -> Option<CaptureStrategy> {
CaptureStrategy::RANKED.iter().copied().find(|candidate| {
candidate.rank() < self.active.rank() && !self.is_disqualified(*candidate)
})
}
fn next_available_below(&self) -> Option<CaptureStrategy> {
let mut candidate = self.active.next_worse();
while let Some(strategy) = candidate {
if !self.is_disqualified(strategy) {
return Some(strategy);
}
candidate = strategy.next_worse();
}
None
}
fn disqualify(&mut self, strategy: CaptureStrategy) {
if !self.disqualified.contains(&strategy) {
self.disqualified.push(strategy);
}
}
pub fn observe(&mut self, signature: FailureSignature) -> FallbackDecision {
if signature.is_terminal_for_all() {
return FallbackDecision::GiveUp {
reason: format!(
"the capture target is gone; no capture strategy can continue (was using \
{})",
self.active.as_str()
),
};
}
if signature == FailureSignature::FramesRecovered {
self.device_lost_streak = 0;
self.stable_frame_streak = self.stable_frame_streak.saturating_add(1);
if self.stable_frame_streak >= STABLE_FRAMES_BEFORE_UPGRADE
&& let Some(target) = self.best_available_above()
{
let from = self.active;
let reason = format!(
"{} has been stable for {} frames; attempting to upgrade back to the \
preferred {} strategy",
from.as_str(),
self.stable_frame_streak,
target.as_str()
);
self.active = target;
self.stable_frame_streak = 0;
self.last_fallback_reason = Some(reason.clone());
return FallbackDecision::Upgrade {
from,
to: target,
reason,
};
}
return FallbackDecision::Stay {
retry_in_place: false,
};
}
self.stable_frame_streak = 0;
if signature == FailureSignature::DeviceLost {
self.device_lost_streak = self.device_lost_streak.saturating_add(1);
if self.device_lost_streak <= MAX_DEVICE_LOST_RETRIES {
return FallbackDecision::Stay {
retry_in_place: true,
};
}
return self.fall_back(
signature,
&format!(
"{} kept losing its capture device ({} consecutive recoveries failed); falling \
back",
self.active.as_str(),
self.device_lost_streak
),
);
}
self.device_lost_streak = 0;
if signature.permanently_disqualifies_current() {
self.disqualify(self.active);
}
let reason = describe_failure(self.active, signature);
self.fall_back(signature, &reason)
}
fn fall_back(&mut self, signature: FailureSignature, reason: &str) -> FallbackDecision {
self.device_lost_streak = 0;
match self.next_available_below() {
Some(target) => {
let from = self.active;
let full = format!("{reason}; switching to {} capture", target.as_str());
self.active = target;
self.last_fallback_reason = Some(full.clone());
FallbackDecision::Transition {
from,
to: target,
reason: full,
}
}
None => {
let give_up = format!(
"{reason}, and no lower-ranked capture strategy is available ({} was the last \
resort, signature={:?})",
self.active.as_str(),
signature
);
self.last_fallback_reason = Some(give_up.clone());
FallbackDecision::GiveUp { reason: give_up }
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FallbackSnapshot {
pub active_strategy: String,
pub last_fallback_reason: String,
}
#[derive(Debug)]
pub struct FallbackTracker {
ranker: FallbackRanker,
}
impl FallbackTracker {
pub fn new(initial: CaptureStrategy) -> Self {
Self {
ranker: FallbackRanker::new(initial),
}
}
pub fn observe(&mut self, signature: FailureSignature) -> FallbackDecision {
self.ranker.observe(signature)
}
pub fn active(&self) -> CaptureStrategy {
self.ranker.active()
}
pub fn snapshot(&self) -> FallbackSnapshot {
FallbackSnapshot {
active_strategy: self.ranker.active().as_str().to_string(),
last_fallback_reason: self.ranker.last_fallback_reason().unwrap_or("").to_string(),
}
}
}
pub fn decision_lifecycle(decision: &FallbackDecision) -> (&'static str, String) {
match decision {
FallbackDecision::Stay { retry_in_place } => (
"diagnostic",
if *retry_in_place {
"capture hit a transient device loss; retrying the current strategy in place"
.to_string()
} else {
"capture is healthy on the current strategy".to_string()
},
),
FallbackDecision::Transition { from, to, reason } => (
"error",
format!(
"fallback: {} -> {} ({reason}) [next-strategy={}]",
from.as_str(),
to.as_str(),
to.as_str()
),
),
FallbackDecision::Upgrade { from, to, reason } => (
"diagnostic",
format!(
"upgrade: {} -> {} ({reason}) [next-strategy={}]",
from.as_str(),
to.as_str(),
to.as_str()
),
),
FallbackDecision::GiveUp { reason } => (
"error",
format!("fallback exhausted: {reason} [next-strategy=none]"),
),
}
}
fn describe_failure(strategy: CaptureStrategy, signature: FailureSignature) -> String {
let what = match signature {
FailureSignature::InjectionFailed => "could not inject its capture hook",
FailureSignature::AntiCheatDenied => {
"is protected by anti-cheat, so the capture hook must not be injected"
}
FailureSignature::NoFramesWithinTimeout => "produced no frames within the start timeout",
FailureSignature::DeviceLost => "lost its capture device",
FailureSignature::UnsupportedTransport => "used a frame transport this build cannot read",
FailureSignature::UnsupportedFormat => "used a swapchain format the fast path cannot read",
FailureSignature::WindowGone => "lost its capture target",
FailureSignature::FramesRecovered => "recovered",
};
format!("{} capture {what}", strategy.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_capture_abi::{
GAME_CAPTURE_FALLBACK_DEVICE_LOST, GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_FORCED_CPU, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED,
GAME_CAPTURE_FALLBACK_MULTISAMPLED, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED,
};
#[test]
fn ranking_order_is_best_to_worst() {
assert!(CaptureStrategy::GameHook.rank() < CaptureStrategy::Wgc.rank());
assert!(CaptureStrategy::Wgc.rank() < CaptureStrategy::DxgiDuplication.rank());
assert!(CaptureStrategy::DxgiDuplication.rank() < CaptureStrategy::WindowGdi.rank());
assert_eq!(
CaptureStrategy::GameHook.next_worse(),
Some(CaptureStrategy::Wgc)
);
assert_eq!(
CaptureStrategy::Wgc.next_worse(),
Some(CaptureStrategy::DxgiDuplication)
);
assert_eq!(
CaptureStrategy::DxgiDuplication.next_worse(),
Some(CaptureStrategy::WindowGdi)
);
assert_eq!(CaptureStrategy::WindowGdi.next_worse(), None);
}
#[test]
fn injection_failure_falls_back_to_wgc() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::InjectionFailed) {
FallbackDecision::Transition { from, to, .. } => {
assert_eq!(from, CaptureStrategy::GameHook);
assert_eq!(to, CaptureStrategy::Wgc);
}
other => panic!("expected Transition, got {other:?}"),
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
assert!(ranker.last_fallback_reason().is_some());
}
#[test]
fn no_frames_timeout_falls_back_but_does_not_disqualify() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::Transition { to, .. } => {
assert_eq!(to, CaptureStrategy::Wgc)
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn wgc_failure_falls_back_to_dxgi() {
let mut ranker = FallbackRanker::new(CaptureStrategy::Wgc);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::Transition { from, to, .. } => {
assert_eq!(from, CaptureStrategy::Wgc);
assert_eq!(to, CaptureStrategy::DxgiDuplication);
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::Wgc));
}
#[test]
fn anti_cheat_denied_disqualifies_game_hook() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
let decision = ranker.observe(FailureSignature::AntiCheatDenied);
assert!(matches!(
decision,
FallbackDecision::Transition {
to: CaptureStrategy::Wgc,
..
}
));
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn device_lost_retries_in_place_then_falls_back() {
let mut ranker = FallbackRanker::new(CaptureStrategy::DxgiDuplication);
for _ in 0..MAX_DEVICE_LOST_RETRIES {
assert_eq!(
ranker.observe(FailureSignature::DeviceLost),
FallbackDecision::Stay {
retry_in_place: true
}
);
assert_eq!(ranker.active(), CaptureStrategy::DxgiDuplication);
}
match ranker.observe(FailureSignature::DeviceLost) {
FallbackDecision::Transition { to, .. } => assert_eq!(to, CaptureStrategy::WindowGdi),
other => panic!("expected Transition after retry budget, got {other:?}"),
}
assert!(!ranker.is_disqualified(CaptureStrategy::DxgiDuplication));
}
#[test]
fn frames_recovered_resets_device_lost_streak() {
let mut ranker = FallbackRanker::new(CaptureStrategy::DxgiDuplication);
ranker.observe(FailureSignature::DeviceLost);
ranker.observe(FailureSignature::DeviceLost);
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
for _ in 0..MAX_DEVICE_LOST_RETRIES {
assert_eq!(
ranker.observe(FailureSignature::DeviceLost),
FallbackDecision::Stay {
retry_in_place: true
}
);
}
}
#[test]
fn window_gone_gives_up_from_any_strategy() {
for start in CaptureStrategy::RANKED {
let mut ranker = FallbackRanker::new(start);
match ranker.observe(FailureSignature::WindowGone) {
FallbackDecision::GiveUp { .. } => {}
other => panic!("expected GiveUp from {start:?}, got {other:?}"),
}
}
}
#[test]
fn last_resort_failure_gives_up() {
let mut ranker = FallbackRanker::new(CaptureStrategy::WindowGdi);
match ranker.observe(FailureSignature::NoFramesWithinTimeout) {
FallbackDecision::GiveUp { reason } => assert!(reason.contains("window-gdi")),
other => panic!("expected GiveUp at last resort, got {other:?}"),
}
}
#[test]
fn full_descent_game_to_wgc_to_dxgi_to_window_then_give_up() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
assert!(matches!(
ranker.observe(FailureSignature::InjectionFailed),
FallbackDecision::Transition {
to: CaptureStrategy::Wgc,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedTransport),
FallbackDecision::Transition {
to: CaptureStrategy::DxgiDuplication,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedTransport),
FallbackDecision::Transition {
to: CaptureStrategy::WindowGdi,
..
}
));
assert!(matches!(
ranker.observe(FailureSignature::UnsupportedFormat),
FallbackDecision::GiveUp { .. }
));
}
#[test]
fn stable_recovery_climbs_back_up_skipping_disqualified() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
ranker.observe(FailureSignature::InjectionFailed);
for _ in 0..=MAX_DEVICE_LOST_RETRIES {
ranker.observe(FailureSignature::DeviceLost);
}
assert_eq!(ranker.active(), CaptureStrategy::DxgiDuplication);
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
match ranker.observe(FailureSignature::FramesRecovered) {
FallbackDecision::Upgrade { from, to, .. } => {
assert_eq!(from, CaptureStrategy::DxgiDuplication);
assert_eq!(to, CaptureStrategy::Wgc);
}
other => panic!("expected Upgrade, got {other:?}"),
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
}
#[test]
fn no_upgrade_when_already_best() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
for _ in 0..(STABLE_FRAMES_BEFORE_UPGRADE + 2) {
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
}
assert_eq!(ranker.active(), CaptureStrategy::GameHook);
}
#[test]
fn upgrade_does_not_revive_disqualified_better_strategy() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
ranker.observe(FailureSignature::AntiCheatDenied);
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
for _ in 0..(STABLE_FRAMES_BEFORE_UPGRADE + 2) {
assert_eq!(
ranker.observe(FailureSignature::FramesRecovered),
FallbackDecision::Stay {
retry_in_place: false
}
);
}
assert_eq!(ranker.active(), CaptureStrategy::Wgc);
}
#[test]
fn fallback_reason_maps_from_abi_reason() {
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_NONE),
None
);
assert_eq!(
FailureSignature::from_fallback_reason(
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED
),
Some(FailureSignature::UnsupportedTransport)
);
assert_eq!(
FailureSignature::from_fallback_reason(
GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED
),
Some(FailureSignature::UnsupportedTransport)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED),
Some(FailureSignature::UnsupportedFormat)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_MULTISAMPLED),
Some(FailureSignature::UnsupportedFormat)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_DEVICE_LOST),
Some(FailureSignature::DeviceLost)
);
assert_eq!(
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORCED_CPU),
None
);
assert_eq!(FailureSignature::from_fallback_reason(u32::MAX), None);
}
#[test]
fn unsupported_transport_disqualifies_current_strategy() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
match ranker.observe(FailureSignature::UnsupportedTransport) {
FallbackDecision::Transition { from, to, reason } => {
assert_eq!(from, CaptureStrategy::GameHook);
assert_eq!(to, CaptureStrategy::Wgc);
assert!(reason.contains("transport"));
}
other => panic!("expected Transition, got {other:?}"),
}
assert!(ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn forced_cpu_reason_is_not_a_failure_signature() {
let mut ranker = FallbackRanker::new(CaptureStrategy::GameHook);
if let Some(signature) =
FailureSignature::from_fallback_reason(GAME_CAPTURE_FALLBACK_FORCED_CPU)
{
ranker.observe(signature);
}
assert_eq!(ranker.active(), CaptureStrategy::GameHook);
assert!(!ranker.is_disqualified(CaptureStrategy::GameHook));
}
#[test]
fn strategy_names_are_stable_for_the_js_seam() {
assert_eq!(CaptureStrategy::GameHook.as_str(), "game-hook");
assert_eq!(CaptureStrategy::Wgc.as_str(), "wgc");
assert_eq!(
CaptureStrategy::DxgiDuplication.as_str(),
"dxgi-duplication"
);
assert_eq!(CaptureStrategy::WindowGdi.as_str(), "window-gdi");
}
#[test]
fn tracker_snapshot_tracks_active_and_reason() {
let mut tracker = FallbackTracker::new(CaptureStrategy::GameHook);
let snap = tracker.snapshot();
assert_eq!(snap.active_strategy, "game-hook");
assert_eq!(snap.last_fallback_reason, "", "no reason before a fallback");
tracker.observe(FailureSignature::InjectionFailed);
let snap = tracker.snapshot();
assert_eq!(snap.active_strategy, "wgc");
assert!(snap.last_fallback_reason.contains("wgc"));
}
#[test]
fn decision_lifecycle_maps_to_event_pairs() {
let transition = FallbackDecision::Transition {
from: CaptureStrategy::GameHook,
to: CaptureStrategy::DxgiDuplication,
reason: "game-hook capture could not inject its capture hook".to_string(),
};
let (kind, msg) = decision_lifecycle(&transition);
assert_eq!(kind, "error");
assert!(msg.contains("next-strategy=dxgi-duplication"));
let upgrade = FallbackDecision::Upgrade {
from: CaptureStrategy::WindowGdi,
to: CaptureStrategy::DxgiDuplication,
reason: "stable".to_string(),
};
let (kind, msg) = decision_lifecycle(&upgrade);
assert_eq!(kind, "diagnostic");
assert!(msg.contains("next-strategy=dxgi-duplication"));
let give_up = FallbackDecision::GiveUp {
reason: "window-gdi was the last resort".to_string(),
};
let (kind, msg) = decision_lifecycle(&give_up);
assert_eq!(kind, "error");
assert!(msg.contains("next-strategy=none"));
let (kind, _) = decision_lifecycle(&FallbackDecision::Stay {
retry_in_place: true,
});
assert_eq!(kind, "diagnostic");
}
}
@@ -1,307 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const GAME_CAPTURE_MAGIC: u32 = 0x4658_4743;
pub const GAME_CAPTURE_ABI_VERSION: u32 = 4;
pub const GAME_CAPTURE_ABI_VERSION_PRESENT_CLOCK: u32 = 4;
pub const GAME_CAPTURE_PRESENT_CLOCK_WALL: u32 = 0;
pub const GAME_CAPTURE_PRESENT_CLOCK_QPC: u32 = 1;
pub const GAME_CAPTURE_STATE_INIT: u32 = 0;
pub const GAME_CAPTURE_STATE_ACTIVE: u32 = 1;
pub const GAME_CAPTURE_STATE_STOPPED: u32 = 2;
pub const GAME_CAPTURE_STATE_ERROR: u32 = 3;
pub const GAME_CAPTURE_STATE_RESIZE_REQUIRED: u32 = 4;
pub const GAME_CAPTURE_BUFFER_COUNT: usize = 2;
pub const GAME_CAPTURE_BYTES_PER_PIXEL: u32 = 4;
pub const GAME_CAPTURE_TRANSPORT_MEMORY: u32 = 0;
pub const GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE: u32 = 1;
pub const GAME_CAPTURE_API_UNKNOWN: u32 = 0;
pub const GAME_CAPTURE_API_OPENGL: u32 = 1;
pub const GAME_CAPTURE_API_D3D8: u32 = 2;
pub const GAME_CAPTURE_API_D3D9: u32 = 3;
pub const GAME_CAPTURE_API_D3D10: u32 = 4;
pub const GAME_CAPTURE_API_D3D11: u32 = 5;
pub const GAME_CAPTURE_API_D3D12: u32 = 6;
pub const GAME_CAPTURE_API_VULKAN: u32 = 7;
pub const GAME_CAPTURE_FALLBACK_NONE: u32 = 0;
pub const GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED: u32 = 1;
pub const GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED: u32 = 2;
pub const GAME_CAPTURE_FALLBACK_FORCED_CPU: u32 = 3;
pub const GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED: u32 = 4;
pub const GAME_CAPTURE_FALLBACK_MULTISAMPLED: u32 = 5;
pub const GAME_CAPTURE_FALLBACK_DEVICE_LOST: u32 = 6;
pub const GAME_CAPTURE_FLAG_HDR: u32 = 1 << 0;
pub const GAME_CAPTURE_FLAG_MULTISAMPLED: u32 = 1 << 1;
pub const GAME_CAPTURE_FLAG_FLIP_VERTICAL: u32 = 1 << 2;
pub const GAME_CAPTURE_FLAG_PROTECTED: u32 = 1 << 3;
pub const GAME_CAPTURE_FLAG_TEN_BIT: u32 = 1 << 4;
pub const ENV_DISABLE_HOOK: &str = "FLUXER_GAME_CAPTURE_DISABLE_HOOK";
pub const ENV_DISABLE_D3D12: &str = "FLUXER_GAME_CAPTURE_DISABLE_D3D12";
pub const ENV_DISABLE_VULKAN: &str = "DISABLE_FLUXER_VULKAN_CAPTURE";
pub const ENV_FORCE_CPU: &str = "FLUXER_GAME_CAPTURE_FORCE_CPU";
pub const ENV_FORCE_SHARED_TEXTURE: &str = "FLUXER_GAME_CAPTURE_FORCE_SHARED_TEXTURE";
pub const ENV_ENABLE_OPENGL_SHARED_TEXTURE: &str =
"FLUXER_GAME_CAPTURE_ENABLE_OPENGL_SHARED_TEXTURE";
pub const ENV_DISABLE_OPENGL_SHARED_TEXTURE: &str =
"FLUXER_GAME_CAPTURE_DISABLE_OPENGL_SHARED_TEXTURE";
pub const ENV_VERBOSE: &str = "FLUXER_GAME_CAPTURE_VERBOSE";
pub const ENV_INJECT_METHOD: &str = "FLUXER_GAME_CAPTURE_INJECT_METHOD";
pub const GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE: u32 = 1 << 0;
pub fn env_flag_enabled(name: &str) -> bool {
std::env::var_os(name)
.map(|value| !value.is_empty())
.unwrap_or(false)
}
pub const GAME_CAPTURE_INFO_PREFIX: &str = "FluxerGameCapture_Info_";
pub const GAME_CAPTURE_FRAME_PREFIX: &str = "FluxerGameCapture_Frame_";
pub const GAME_CAPTURE_READY_PREFIX: &str = "FluxerGameCapture_Ready_";
pub const GAME_CAPTURE_STOP_PREFIX: &str = "FluxerGameCapture_Stop_";
pub const GAME_CAPTURE_KEEPALIVE_PREFIX: &str = "FluxerGameCapture_KeepAlive_";
pub const GAME_CAPTURE_MUTEX_PREFIX: &str = "FluxerGameCapture_FrameMutex_";
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GameCaptureSharedInfo {
pub magic: u32,
pub version: u32,
pub state: u32,
pub last_error: u32,
pub hwnd: u64,
pub max_width: u32,
pub max_height: u32,
pub width: u32,
pub height: u32,
pub pitch: u32,
pub frame_index: u32,
pub frame_counter: u64,
pub timestamp_us: i64,
pub target_frame_interval_ns: u64,
pub transport: u32,
pub dxgi_format: u32,
pub texture_handle: u64,
pub api_type: u32,
pub fallback_reason: u32,
pub capture_flags: u32,
pub dropped_frame_counter: u64,
pub last_present_timestamp_us: i64,
pub control: u32,
pub present_clock: u32,
pub reserved: [u32; 23],
}
const _: () = assert!(std::mem::size_of::<GameCaptureSharedInfo>() == 224);
impl GameCaptureSharedInfo {
pub fn new(hwnd: u64, max_width: u32, max_height: u32, frame_rate: u32) -> Self {
Self {
magic: GAME_CAPTURE_MAGIC,
version: GAME_CAPTURE_ABI_VERSION,
state: GAME_CAPTURE_STATE_INIT,
last_error: 0,
hwnd,
max_width,
max_height,
width: 0,
height: 0,
pitch: max_width.saturating_mul(GAME_CAPTURE_BYTES_PER_PIXEL),
frame_index: 0,
frame_counter: 0,
timestamp_us: 0,
target_frame_interval_ns: frame_interval_ns(frame_rate),
transport: GAME_CAPTURE_TRANSPORT_MEMORY,
dxgi_format: 0,
texture_handle: 0,
api_type: GAME_CAPTURE_API_UNKNOWN,
fallback_reason: GAME_CAPTURE_FALLBACK_NONE,
capture_flags: 0,
dropped_frame_counter: 0,
last_present_timestamp_us: 0,
control: 0,
present_clock: GAME_CAPTURE_PRESENT_CLOCK_WALL,
reserved: [0; 23],
}
}
}
pub fn host_supports_present_clock(version: u32) -> bool {
version >= GAME_CAPTURE_ABI_VERSION_PRESENT_CLOCK
}
pub fn qpc_ticks_to_us(ticks: i64, frequency: i64) -> i64 {
if frequency <= 0 {
return 0;
}
if ticks <= 0 {
return 0;
}
let whole_second_us = (ticks / frequency).saturating_mul(1_000_000);
let fractional_us = (ticks % frequency).saturating_mul(1_000_000) / frequency;
whole_second_us.saturating_add(fractional_us)
}
pub fn presented_recently(
present_clock: u32,
last_present_us: i64,
wall_now_us: i64,
qpc_now_us: i64,
window_us: i64,
) -> bool {
assert!(window_us > 0);
if last_present_us <= 0 {
return false;
}
let now_us = if present_clock == GAME_CAPTURE_PRESENT_CLOCK_QPC {
qpc_now_us
} else {
wall_now_us
};
now_us.saturating_sub(last_present_us) < window_us
}
#[cfg(target_os = "windows")]
pub fn qpc_now_us() -> i64 {
use windows_sys::Win32::System::Performance::{
QueryPerformanceCounter, QueryPerformanceFrequency,
};
let mut ticks = 0i64;
let mut frequency = 0i64;
let counter_ok = unsafe { QueryPerformanceCounter(&mut ticks) };
let frequency_ok = unsafe { QueryPerformanceFrequency(&mut frequency) };
if counter_ok == 0 || frequency_ok == 0 {
return 0;
}
qpc_ticks_to_us(ticks, frequency)
}
pub fn frame_interval_ns(frame_rate: u32) -> u64 {
let rate = frame_rate.clamp(1, 144) as u64;
1_000_000_000 / rate
}
pub fn frame_buffer_stride(width: u32) -> u32 {
width.saturating_mul(GAME_CAPTURE_BYTES_PER_PIXEL)
}
pub fn frame_buffer_size(width: u32, height: u32) -> Option<usize> {
frame_buffer_stride(width)
.checked_mul(height)?
.try_into()
.ok()
}
pub fn shared_frame_mapping_size(width: u32, height: u32) -> Option<usize> {
frame_buffer_size(width, height)?.checked_mul(GAME_CAPTURE_BUFFER_COUNT)
}
pub fn object_name(prefix: &str, pid: u32) -> String {
format!("{prefix}{pid}")
}
pub fn mutex_name(pid: u32, index: usize) -> String {
format!("{GAME_CAPTURE_MUTEX_PREFIX}{index}_{pid}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qpc_ticks_to_us_converts_whole_and_fractional_seconds() {
assert_eq!(qpc_ticks_to_us(10_000_000, 10_000_000), 1_000_000);
assert_eq!(qpc_ticks_to_us(15_000_000, 10_000_000), 1_500_000);
assert_eq!(qpc_ticks_to_us(1, 10_000_000), 0);
assert_eq!(qpc_ticks_to_us(10, 10_000_000), 1);
}
#[test]
fn qpc_ticks_to_us_does_not_overflow_for_long_uptimes() {
let frequency = 3_800_000_000i64;
let uptime_seconds = 400 * 24 * 60 * 60i64;
let ticks = frequency * uptime_seconds;
assert_eq!(
qpc_ticks_to_us(ticks, frequency),
uptime_seconds * 1_000_000
);
}
#[test]
fn qpc_ticks_to_us_rejects_invalid_inputs() {
assert_eq!(qpc_ticks_to_us(123, 0), 0);
assert_eq!(qpc_ticks_to_us(123, -1), 0);
assert_eq!(qpc_ticks_to_us(-123, 10_000_000), 0);
assert_eq!(qpc_ticks_to_us(0, 10_000_000), 0);
}
#[test]
fn host_supports_present_clock_gates_on_abi_version() {
assert!(!host_supports_present_clock(0));
assert!(!host_supports_present_clock(3));
assert!(host_supports_present_clock(4));
assert!(host_supports_present_clock(5));
}
#[test]
fn presented_recently_uses_wall_clock_for_legacy_hooks() {
assert!(presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
1_000_000,
1_500_000,
i64::MAX,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
1_000_000,
2_000_000,
0,
1_000_000
));
}
#[test]
fn presented_recently_uses_qpc_for_present_clock_hooks() {
assert!(presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
1_000_000,
i64::MAX,
1_500_000,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
1_000_000,
0,
2_000_000,
1_000_000
));
}
#[test]
fn presented_recently_requires_an_observed_present() {
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_WALL,
0,
1,
1,
1_000_000
));
assert!(!presented_recently(
GAME_CAPTURE_PRESENT_CLOCK_QPC,
-1,
1,
1,
1_000_000
));
}
}
@@ -1,465 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const DXGI_FORMAT_R16G16B16A16_FLOAT: u32 = 10;
pub const DXGI_FORMAT_R16G16B16A16_UNORM: u32 = 11;
pub const DXGI_FORMAT_R10G10B10A2_UNORM: u32 = 24;
pub const DXGI_FORMAT_R8G8B8A8_UNORM: u32 = 28;
pub const DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: u32 = 29;
pub const DXGI_FORMAT_B8G8R8A8_UNORM: u32 = 87;
pub const DXGI_FORMAT_B8G8R8X8_UNORM: u32 = 88;
pub const DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM: u32 = 89;
pub const DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: u32 = 91;
pub const DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: u32 = 93;
pub const DXGI_FORMAT_P010: u32 = 104;
pub const DXGI_FORMAT_P016: u32 = 105;
pub const DXGI_FORMAT_420_OPAQUE: u32 = 106;
pub const DXGI_FORMAT_YUY2: u32 = 107;
pub const DXGI_FORMAT_R32G32B32A32_FLOAT: u32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceFormat {
Bgra8,
Rgba8,
R10G10B10A2 { hdr: bool },
Rgba16Float { hdr: bool },
}
impl SourceFormat {
pub fn classify(dxgi_format: u32, capture_flags: u32) -> Option<SourceFormat> {
let hdr = capture_flags & crate::game_capture_abi::GAME_CAPTURE_FLAG_HDR != 0;
match dxgi_format {
DXGI_FORMAT_B8G8R8A8_UNORM
| DXGI_FORMAT_B8G8R8A8_UNORM_SRGB
| DXGI_FORMAT_B8G8R8X8_UNORM
| DXGI_FORMAT_B8G8R8X8_UNORM_SRGB => Some(SourceFormat::Bgra8),
DXGI_FORMAT_R8G8B8A8_UNORM | DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => {
Some(SourceFormat::Rgba8)
}
DXGI_FORMAT_R10G10B10A2_UNORM => Some(SourceFormat::R10G10B10A2 { hdr }),
DXGI_FORMAT_R16G16B16A16_FLOAT => Some(SourceFormat::Rgba16Float { hdr }),
_ => None,
}
}
pub fn is_8bit(self) -> bool {
matches!(self, SourceFormat::Bgra8 | SourceFormat::Rgba8)
}
pub fn is_hdr(self) -> bool {
matches!(
self,
SourceFormat::R10G10B10A2 { hdr: true } | SourceFormat::Rgba16Float { hdr: true }
)
}
pub fn bytes_per_pixel(self) -> usize {
match self {
SourceFormat::Bgra8 | SourceFormat::Rgba8 | SourceFormat::R10G10B10A2 { .. } => 4,
SourceFormat::Rgba16Float { .. } => 8,
}
}
}
const SDR_WHITE_NITS: f32 = 80.0;
pub fn r10g10b10a2_row_to_bgra(src_row: &[u8], width: usize, dst_row: &mut [u8], hdr: bool) {
for x in 0..width {
let so = x * 4;
let dofs = x * 4;
if so + 4 > src_row.len() || dofs + 4 > dst_row.len() {
break;
}
let packed = u32::from_le_bytes([
src_row[so],
src_row[so + 1],
src_row[so + 2],
src_row[so + 3],
]);
let r10 = (packed & 0x3FF) as u16;
let g10 = ((packed >> 10) & 0x3FF) as u16;
let b10 = ((packed >> 20) & 0x3FF) as u16;
let a2 = ((packed >> 30) & 0x3) as u8;
let (r, g, b) = if hdr {
tonemap_rec2020_pq_to_srgb8(r10, g10, b10)
} else {
(scale10_to_8(r10), scale10_to_8(g10), scale10_to_8(b10))
};
dst_row[dofs] = b;
dst_row[dofs + 1] = g;
dst_row[dofs + 2] = r;
dst_row[dofs + 3] = (a2 as u16 * 255 / 3) as u8;
}
}
pub fn rgba16f_row_to_bgra(src_row: &[u8], width: usize, dst_row: &mut [u8], hdr: bool) {
for x in 0..width {
let so = x * 8;
let dofs = x * 4;
if so + 8 > src_row.len() || dofs + 4 > dst_row.len() {
break;
}
let r = f16_to_f32(u16::from_le_bytes([src_row[so], src_row[so + 1]]));
let g = f16_to_f32(u16::from_le_bytes([src_row[so + 2], src_row[so + 3]]));
let b = f16_to_f32(u16::from_le_bytes([src_row[so + 4], src_row[so + 5]]));
let a = f16_to_f32(u16::from_le_bytes([src_row[so + 6], src_row[so + 7]]));
let (lr, lg, lb) = if hdr {
(
reinhard(r.max(0.0)),
reinhard(g.max(0.0)),
reinhard(b.max(0.0)),
)
} else {
(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
};
dst_row[dofs] = linear_to_srgb8(lb);
dst_row[dofs + 1] = linear_to_srgb8(lg);
dst_row[dofs + 2] = linear_to_srgb8(lr);
dst_row[dofs + 3] = (a.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
}
fn scale10_to_8(v10: u16) -> u8 {
((v10 as u32 * 255 + 511) / 1023) as u8
}
fn reinhard(linear: f32) -> f32 {
let v = linear.max(0.0);
(v / (1.0 + v)).clamp(0.0, 1.0)
}
fn tonemap_rec2020_pq_to_srgb8(r10: u16, g10: u16, b10: u16) -> (u8, u8, u8) {
let lr = pq_eotf(r10 as f32 / 1023.0);
let lg = pq_eotf(g10 as f32 / 1023.0);
let lb = pq_eotf(b10 as f32 / 1023.0);
let scale = 10000.0 / SDR_WHITE_NITS;
let map = |v: f32| reinhard((v * scale).max(0.0));
(
linear_to_srgb8(map(lr)),
linear_to_srgb8(map(lg)),
linear_to_srgb8(map(lb)),
)
}
fn pq_eotf(e: f32) -> f32 {
const M1: f64 = 0.1593017578125;
const M2: f64 = 78.84375;
const C1: f64 = 0.8359375;
const C2: f64 = 18.8515625;
const C3: f64 = 18.6875;
let e = (e.clamp(0.0, 1.0)) as f64;
let ep = e.powf(1.0 / M2);
let num = (ep - C1).max(0.0);
let den = C2 - C3 * ep;
if den <= 0.0 {
return 0.0;
}
(num / den).powf(1.0 / M1) as f32
}
fn linear_to_srgb8(linear: f32) -> u8 {
let l = linear.clamp(0.0, 1.0);
let srgb = if l <= 0.0031308 {
l * 12.92
} else {
1.055 * l.powf(1.0 / 2.4) - 0.055
};
(srgb.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}
pub fn f16_to_f32(h: u16) -> f32 {
let sign = (h >> 15) & 0x1;
let exp = (h >> 10) & 0x1F;
let mant = h & 0x3FF;
let sign_f = if sign == 1 { -1.0f32 } else { 1.0f32 };
if exp == 0 {
sign_f * (mant as f32) * 2f32.powi(-24)
} else if exp == 0x1F {
if mant == 0 { sign_f * 65504.0 } else { 0.0 }
} else {
sign_f * (1.0 + (mant as f32) / 1024.0) * 2f32.powi(exp as i32 - 15)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_capture_abi::{GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_TEN_BIT};
#[test]
fn classifies_8bit_formats() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_B8G8R8A8_UNORM, 0),
Some(SourceFormat::Bgra8)
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R8G8B8A8_UNORM, 0),
Some(SourceFormat::Rgba8)
);
assert!(
SourceFormat::classify(DXGI_FORMAT_B8G8R8A8_UNORM, 0)
.unwrap()
.is_8bit()
);
}
#[test]
fn classifies_obs_8bit_alias_set() {
for format in [
DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
DXGI_FORMAT_B8G8R8X8_UNORM,
DXGI_FORMAT_B8G8R8X8_UNORM_SRGB,
] {
assert_eq!(SourceFormat::classify(format, 0), Some(SourceFormat::Bgra8));
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Bgra8),
"8-bit format {format} must not become HDR just because the flag is set"
);
}
for format in [DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB] {
assert_eq!(SourceFormat::classify(format, 0), Some(SourceFormat::Rgba8));
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Rgba8),
"8-bit format {format} must not become HDR just because the flag is set"
);
}
}
#[test]
fn classifies_10bit_and_hdr_flag() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R10G10B10A2_UNORM, GAME_CAPTURE_FLAG_TEN_BIT),
Some(SourceFormat::R10G10B10A2 { hdr: false })
);
assert_eq!(
SourceFormat::classify(
DXGI_FORMAT_R10G10B10A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR
),
Some(SourceFormat::R10G10B10A2 { hdr: true })
);
assert!(
SourceFormat::classify(DXGI_FORMAT_R10G10B10A2_UNORM, GAME_CAPTURE_FLAG_HDR)
.unwrap()
.is_hdr()
);
}
#[test]
fn high_precision_formats_have_expected_byte_widths() {
assert_eq!(SourceFormat::Bgra8.bytes_per_pixel(), 4);
assert_eq!(SourceFormat::Rgba8.bytes_per_pixel(), 4);
assert_eq!(
SourceFormat::R10G10B10A2 { hdr: false }.bytes_per_pixel(),
4
);
assert_eq!(SourceFormat::R10G10B10A2 { hdr: true }.bytes_per_pixel(), 4);
assert_eq!(
SourceFormat::Rgba16Float { hdr: false }.bytes_per_pixel(),
8
);
assert_eq!(SourceFormat::Rgba16Float { hdr: true }.bytes_per_pixel(), 8);
}
#[test]
fn classifies_fp16_scrgb() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_FLOAT, GAME_CAPTURE_FLAG_HDR),
Some(SourceFormat::Rgba16Float { hdr: true })
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_FLOAT, 0),
Some(SourceFormat::Rgba16Float { hdr: false })
);
assert_eq!(
SourceFormat::Rgba16Float { hdr: false }.bytes_per_pixel(),
8
);
}
#[test]
fn unsupported_format_is_none() {
assert_eq!(SourceFormat::classify(104, 0), None);
assert_eq!(SourceFormat::classify(0, 0), None);
}
#[test]
fn video_and_opaque_formats_stay_unsupported_even_with_hdr_flags() {
for format in [
DXGI_FORMAT_P010,
DXGI_FORMAT_P016,
DXGI_FORMAT_420_OPAQUE,
DXGI_FORMAT_YUY2,
] {
assert_eq!(
SourceFormat::classify(format, GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR),
None,
"DXGI video/opaque format {format} is not a directly readable game backbuffer"
);
}
}
#[test]
fn obs_high_precision_formats_without_reader_conversion_stay_unsupported() {
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R16G16B16A16_UNORM, GAME_CAPTURE_FLAG_HDR),
None,
"OBS maps RGBA16 UNORM, but Fluxer needs a row converter before claiming support"
);
assert_eq!(
SourceFormat::classify(DXGI_FORMAT_R32G32B32A32_FLOAT, GAME_CAPTURE_FLAG_HDR),
None,
"OBS maps RGBA32F, but Fluxer has no reader-side conversion for it yet"
);
assert_eq!(
SourceFormat::classify(
DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR
),
None,
"XR-bias 10A2 is not equivalent to regular R10G10B10A2 UNORM"
);
}
#[test]
fn f16_round_trips_known_values() {
assert_eq!(f16_to_f32(0x0000), 0.0);
assert_eq!(f16_to_f32(0x3C00), 1.0);
assert_eq!(f16_to_f32(0x4000), 2.0);
assert_eq!(f16_to_f32(0x3800), 0.5);
assert!((f16_to_f32(0xC000) + 2.0).abs() < 1e-6);
}
#[test]
fn f16_special_values_are_bounded_for_sdr_fallback() {
assert_eq!(f16_to_f32(0x7C00), 65504.0);
assert_eq!(f16_to_f32(0xFC00), -65504.0);
assert_eq!(f16_to_f32(0x7E00), 0.0);
}
#[test]
fn r10_sdr_unpack_scales_extremes() {
let packed: u32 = 0x3FF | (0x3FF << 10) | (0x3FF << 20) | (0x3 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst, [255, 255, 255, 255]);
let src0 = 0u32.to_le_bytes();
let mut dst0 = [0u8; 4];
r10g10b10a2_row_to_bgra(&src0, 1, &mut dst0, false);
assert_eq!(dst0, [0, 0, 0, 0]);
}
#[test]
fn r10_sdr_unpack_scales_midpoints_and_alpha() {
let packed: u32 = 0x200 | (0x100 << 10) | (0x080 << 20) | (0x2 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert!((31..=33).contains(&dst[0]), "B was {}", dst[0]);
assert!((63..=65).contains(&dst[1]), "G was {}", dst[1]);
assert!((127..=129).contains(&dst[2]), "R was {}", dst[2]);
assert_eq!(dst[3], 170);
}
#[test]
fn r10_unpack_respects_short_rows() {
let src = [0xFF, 0xFF, 0xFF];
let mut dst = [9u8; 8];
r10g10b10a2_row_to_bgra(&src, 2, &mut dst, false);
assert_eq!(dst, [9u8; 8]);
}
#[test]
fn r10_unpack_channel_order_is_bgra() {
let packed: u32 = 0x3FF;
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst[0], 0, "B should be 0");
assert_eq!(dst[1], 0, "G should be 0");
assert_eq!(dst[2], 255, "R should be 255");
}
#[test]
fn r10_hdr_unpack_is_bounded_and_nonzero() {
let packed: u32 = 0x3FF | (0x200 << 10) | (0x100 << 20) | (0x3 << 30);
let src = packed.to_le_bytes();
let mut dst = [0u8; 4];
r10g10b10a2_row_to_bgra(&src, 1, &mut dst, true);
assert!(dst[2] > 0, "bright red PQ should map to a visible value");
}
#[test]
fn r10_hdr_unpack_is_monotonic_after_tonemap() {
let pack = |v: u32| -> [u8; 4] { (v | (v << 10) | (v << 20) | (0x3 << 30)).to_le_bytes() };
let mut low = [0u8; 4];
let mut mid = [0u8; 4];
let mut high = [0u8; 4];
r10g10b10a2_row_to_bgra(&pack(0x100), 1, &mut low, true);
r10g10b10a2_row_to_bgra(&pack(0x200), 1, &mut mid, true);
r10g10b10a2_row_to_bgra(&pack(0x3FF), 1, &mut high, true);
assert!(low[2] <= mid[2] && mid[2] <= high[2]);
assert_eq!(high[3], 255);
}
#[test]
fn fp16_sdr_clamps_and_encodes_srgb() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0x3C00u16.to_le_bytes());
src[6..8].copy_from_slice(&0x3C00u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, false);
assert_eq!(dst[0], 0, "B");
assert_eq!(dst[1], 0, "G");
assert_eq!(dst[2], 255, "R linear 1.0 -> sRGB 255");
assert_eq!(dst[3], 255, "A");
}
#[test]
fn fp16_sdr_clamps_negative_and_above_one_channels() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0xBC00u16.to_le_bytes());
src[2..4].copy_from_slice(&0x4000u16.to_le_bytes());
src[4..6].copy_from_slice(&0x3800u16.to_le_bytes());
src[6..8].copy_from_slice(&0x4000u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, false);
assert!((180..=196).contains(&dst[0]), "B linear 0.5 was {}", dst[0]);
assert_eq!(dst[1], 255, "G linear 2.0 clamps to white");
assert_eq!(dst[2], 0, "R negative clamps to black");
assert_eq!(dst[3], 255, "A above 1.0 clamps opaque");
}
#[test]
fn fp16_unpack_respects_short_rows() {
let src = [0x00, 0x3C, 0x00, 0x3C];
let mut dst = [7u8; 8];
rgba16f_row_to_bgra(&src, 2, &mut dst, false);
assert_eq!(dst, [7u8; 8]);
}
#[test]
fn fp16_hdr_above_one_is_tonemapped_not_clipped_to_garbage() {
let mut src = [0u8; 8];
src[0..2].copy_from_slice(&0x4400u16.to_le_bytes());
src[6..8].copy_from_slice(&0x3C00u16.to_le_bytes());
let mut dst = [0u8; 4];
rgba16f_row_to_bgra(&src, 1, &mut dst, true);
assert!(dst[2] > 100, "HDR highlight should be bright after tonemap");
}
#[test]
fn srgb_encode_endpoints() {
assert_eq!(linear_to_srgb8(0.0), 0);
assert_eq!(linear_to_srgb8(1.0), 255);
let mid = linear_to_srgb8(0.5);
assert!((180..=196).contains(&mid), "sRGB(0.5) was {mid}");
}
}
File diff suppressed because it is too large. Load diff
@@ -1,365 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_RESOURCE_MISC_SHARED, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC,
D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0,
D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0,
D3D11_VIDEO_PROCESSOR_STREAM, D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
D3D11_VPIV_DIMENSION_TEXTURE2D, D3D11_VPOV_DIMENSION_TEXTURE2D, ID3D11Device,
ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoContext1,
ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator,
ID3D11VideoProcessorInputView, ID3D11VideoProcessorOutputView,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709, DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, DXGI_COLOR_SPACE_TYPE,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, DXGI_FORMAT_NV12, DXGI_RATIONAL, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIResource;
use windows::core::Interface;
use crate::hdr;
fn vlog(msg: &str) {
if crate::game_capture_abi::env_flag_enabled(crate::game_capture_abi::ENV_VERBOSE) {
use std::io::Write;
let _ = writeln!(std::io::stderr(), "[fluxer-nv12] {msg}");
}
}
pub const NV12_OUTPUT_SLOT_COUNT: usize = 3;
struct Nv12OutputSlot {
_texture: ID3D11Texture2D,
view: ID3D11VideoProcessorOutputView,
handle: u64,
}
pub struct Nv12GpuConverter {
_video_device: ID3D11VideoDevice,
video_context: ID3D11VideoContext,
processor: ID3D11VideoProcessor,
_enumerator: ID3D11VideoProcessorEnumerator,
input_view: ID3D11VideoProcessorInputView,
output_slots: [Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT],
slot_cursor: usize,
context: ID3D11DeviceContext,
out_width: u32,
out_height: u32,
}
pub struct Nv12SharedTextureFrame {
pub handle: u64,
pub width: u32,
pub height: u32,
pub dxgi_format: u32,
}
impl Nv12GpuConverter {
#[allow(clippy::too_many_arguments)]
pub fn new(
device: &ID3D11Device,
context: &ID3D11DeviceContext,
input: &ID3D11Texture2D,
in_width: u32,
in_height: u32,
out_width: u32,
out_height: u32,
source_format: hdr::SourceFormat,
) -> Option<Self> {
let out_width = (out_width & !1).max(2);
let out_height = (out_height & !1).max(2);
let video_device = device
.cast::<ID3D11VideoDevice>()
.inspect_err(|e| vlog(&format!("cast ID3D11VideoDevice failed: {e:?}")))
.ok()?;
let video_context = context
.cast::<ID3D11VideoContext>()
.inspect_err(|e| vlog(&format!("cast ID3D11VideoContext failed: {e:?}")))
.ok()?;
let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
InputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
InputWidth: in_width,
InputHeight: in_height,
OutputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
OutputWidth: out_width,
OutputHeight: out_height,
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
};
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content_desc) }
.inspect_err(|e| vlog(&format!("CreateVideoProcessorEnumerator: {e:?}")))
.ok()?;
let processor = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
.inspect_err(|e| vlog(&format!("CreateVideoProcessor: {e:?}")))
.ok()?;
if let Ok(vctx1) = video_context.cast::<ID3D11VideoContext1>() {
let input_cs = input_colour_space(source_format);
unsafe {
vctx1.VideoProcessorSetStreamColorSpace1(&processor, 0, input_cs);
vctx1.VideoProcessorSetOutputColorSpace1(
&processor,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709,
);
}
vlog(&format!(
"video processor colour space set: input={} -> output=YCbCr studio Rec.709",
input_cs.0
));
} else {
vlog("ID3D11VideoContext1 unavailable; using default SDR Rec.709 colour space");
}
let output_desc = D3D11_TEXTURE2D_DESC {
Width: out_width,
Height: out_height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_NV12,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut output_slots = Vec::with_capacity(NV12_OUTPUT_SLOT_COUNT);
for _ in 0..NV12_OUTPUT_SLOT_COUNT {
output_slots.push(create_output_slot(
device,
&video_device,
&enumerator,
&output_desc,
)?);
}
assert_eq!(
output_slots.len(),
NV12_OUTPUT_SLOT_COUNT,
"all NV12 output slots created"
);
let Ok(output_slots) = <[Nv12OutputSlot; NV12_OUTPUT_SLOT_COUNT]>::try_from(output_slots)
else {
vlog("NV12 output slot count mismatch");
return None;
};
let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
FourCC: 0,
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPIV {
MipSlice: 0,
ArraySlice: 0,
},
},
};
let mut input_view = None;
unsafe {
video_device.CreateVideoProcessorInputView(
input,
&enumerator,
&input_view_desc,
Some(&mut input_view),
)
}
.inspect_err(|e| vlog(&format!("CreateVideoProcessorInputView: {e:?}")))
.ok()?;
let input_view = input_view?;
vlog(&format!(
"NV12 converter built OK ({in_width}x{in_height} -> {out_width}x{out_height})"
));
Some(Self {
_video_device: video_device,
video_context,
processor,
_enumerator: enumerator,
input_view,
output_slots,
slot_cursor: 0,
context: context.clone(),
out_width,
out_height,
})
}
pub fn dxgi_format(&self) -> u32 {
DXGI_FORMAT_NV12.0 as u32
}
pub fn convert_shared_texture(&mut self) -> Result<Nv12SharedTextureFrame, String> {
assert!(
self.slot_cursor < NV12_OUTPUT_SLOT_COUNT,
"slot cursor in range"
);
assert!(self.out_width >= 2, "output width at least 2");
let slot_index = self.slot_cursor;
self.slot_cursor = (slot_index + 1) % NV12_OUTPUT_SLOT_COUNT;
self.run_video_processor(slot_index)?;
unsafe {
self.context.Flush();
}
Ok(Nv12SharedTextureFrame {
handle: self.output_slots[slot_index].handle,
width: self.out_width,
height: self.out_height,
dxgi_format: self.dxgi_format(),
})
}
fn run_video_processor(&self, slot_index: usize) -> Result<(), String> {
assert!(slot_index < NV12_OUTPUT_SLOT_COUNT, "slot index in range");
let mut stream = D3D11_VIDEO_PROCESSOR_STREAM {
Enable: windows::core::BOOL(1),
OutputIndex: 0,
InputFrameOrField: 0,
PastFrames: 0,
FutureFrames: 0,
ppPastSurfaces: std::ptr::null_mut(),
pInputSurface: std::mem::ManuallyDrop::new(Some(self.input_view.clone())),
ppFutureSurfaces: std::ptr::null_mut(),
ppPastSurfacesRight: std::ptr::null_mut(),
pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
ppFutureSurfacesRight: std::ptr::null_mut(),
};
let blt = unsafe {
self.video_context.VideoProcessorBlt(
&self.processor,
&self.output_slots[slot_index].view,
0,
std::slice::from_ref(&stream),
)
};
unsafe {
std::mem::ManuallyDrop::drop(&mut stream.pInputSurface);
}
blt.inspect_err(|e| vlog(&format!("VideoProcessorBlt RGB->NV12: {e:?}")))
.map_err(|e| format!("VideoProcessorBlt RGB->NV12: {e}"))
}
}
fn create_output_slot(
device: &ID3D11Device,
video_device: &ID3D11VideoDevice,
enumerator: &ID3D11VideoProcessorEnumerator,
output_desc: &D3D11_TEXTURE2D_DESC,
) -> Option<Nv12OutputSlot> {
assert!(output_desc.Width >= 2, "output width at least 2");
assert!(output_desc.Height >= 2, "output height at least 2");
let mut output_texture = None;
unsafe { device.CreateTexture2D(output_desc, None, Some(&mut output_texture)) }
.inspect_err(|e| vlog(&format!("CreateTexture2D NV12 output: {e:?}")))
.ok()?;
let output_texture = output_texture?;
let resource: IDXGIResource = output_texture
.cast()
.inspect_err(|e| {
vlog(&format!(
"QueryInterface IDXGIResource for NV12 output: {e:?}"
))
})
.ok()?;
let shared_handle = unsafe { resource.GetSharedHandle() }
.inspect_err(|e| vlog(&format!("GetSharedHandle NV12 output: {e:?}")))
.ok()?;
if shared_handle.is_invalid() {
vlog("GetSharedHandle NV12 output returned an invalid handle");
return None;
}
let shared_handle = shared_handle.0 as usize as u64;
let output_view_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
Texture2D: windows::Win32::Graphics::Direct3D11::D3D11_TEX2D_VPOV { MipSlice: 0 },
},
};
let mut output_view = None;
unsafe {
video_device.CreateVideoProcessorOutputView(
&output_texture,
enumerator,
&output_view_desc,
Some(&mut output_view),
)
}
.inspect_err(|e| vlog(&format!("CreateVideoProcessorOutputView: {e:?}")))
.ok()?;
let output_view = output_view?;
Some(Nv12OutputSlot {
_texture: output_texture,
view: output_view,
handle: shared_handle,
})
}
fn input_colour_space(source_format: hdr::SourceFormat) -> DXGI_COLOR_SPACE_TYPE {
match source_format {
hdr::SourceFormat::R10G10B10A2 { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020,
hdr::SourceFormat::Rgba16Float { hdr: true } => DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709,
_ => DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709,
}
}
unsafe impl Send for Nv12GpuConverter {}
#[cfg(test)]
mod tests {
use super::*;
fn cs_value(source_format: hdr::SourceFormat) -> i32 {
input_colour_space(source_format).0
}
#[test]
fn eight_bit_sources_use_sdr_rec709_colour_space() {
assert_eq!(
cs_value(hdr::SourceFormat::Bgra8),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
assert_eq!(
cs_value(hdr::SourceFormat::Rgba8),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
}
#[test]
fn unflagged_high_precision_sources_stay_sdr_rec709() {
assert_eq!(
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: false }),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
assert_eq!(
cs_value(hdr::SourceFormat::Rgba16Float { hdr: false }),
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709.0
);
}
#[test]
fn ten_bit_hdr_uses_pq_rec2020_input_space() {
assert_eq!(
cs_value(hdr::SourceFormat::R10G10B10A2 { hdr: true }),
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020.0
);
}
#[test]
fn fp16_hdr_uses_linear_extended_rec709_input_space() {
assert_eq!(
cs_value(hdr::SourceFormat::Rgba16Float { hdr: true }),
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709.0
);
}
}
@@ -1,168 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::ScreenCaptureSourceDescriptor;
#[cfg(target_os = "windows")]
use std::ptr::null_mut;
#[cfg(target_os = "windows")]
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, RECT},
Graphics::Gdi::{EnumDisplayMonitors, HMONITOR},
System::Threading::GetCurrentProcessId,
UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetWindow, GetWindowLongPtrW, GetWindowRect,
GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, WS_CHILD,
},
};
#[cfg(target_os = "windows")]
const MONITOR_SOURCE_LIMIT: usize = 16;
pub fn list_sources() -> Vec<ScreenCaptureSourceDescriptor> {
#[cfg(target_os = "windows")]
{
let mut sources = enumerate_monitor_sources();
sources.extend(enumerate_window_sources());
sources
}
#[cfg(not(target_os = "windows"))]
{
Vec::new()
}
}
#[cfg(target_os = "windows")]
fn enumerate_monitor_sources() -> Vec<ScreenCaptureSourceDescriptor> {
unsafe extern "system" fn enum_monitor(
_monitor: HMONITOR,
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
rect: *mut RECT,
param: LPARAM,
) -> i32 {
let monitors = &mut *(param as *mut Vec<RECT>);
if monitors.len() >= MONITOR_SOURCE_LIMIT {
return 0;
}
if !rect.is_null() {
monitors.push(*rect);
}
1
}
let mut monitors: Vec<RECT> = Vec::new();
unsafe {
EnumDisplayMonitors(
null_mut(),
null_mut(),
Some(enum_monitor),
&mut monitors as *mut _ as LPARAM,
);
}
assert!(
monitors.len() <= MONITOR_SOURCE_LIMIT,
"monitor source enumeration bounded"
);
monitors
.into_iter()
.enumerate()
.map(|(index, rect)| {
let width = (rect.right - rect.left).max(0) as u32;
let height = (rect.bottom - rect.top).max(0) as u32;
ScreenCaptureSourceDescriptor {
kind: "screen".to_string(),
id: format!("screen:{index}:0"),
name: format!("Display {}", index + 1),
width,
height,
target_pid: None,
}
})
.collect()
}
#[cfg(target_os = "windows")]
fn enumerate_window_sources() -> Vec<ScreenCaptureSourceDescriptor> {
struct EnumState {
own_pid: u32,
sources: Vec<ScreenCaptureSourceDescriptor>,
}
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
let state = &mut *(param as *mut EnumState);
if let Some(source) = describe_window_source(hwnd, state.own_pid) {
state.sources.push(source);
}
1
}
let mut state = EnumState {
own_pid: unsafe { GetCurrentProcessId() },
sources: Vec::new(),
};
unsafe {
EnumWindows(Some(enum_window), &mut state as *mut _ as LPARAM);
}
state.sources
}
#[cfg(target_os = "windows")]
fn describe_window_source(hwnd: HWND, own_pid: u32) -> Option<ScreenCaptureSourceDescriptor> {
if hwnd.is_null() || unsafe { IsWindowVisible(hwnd) } == 0 {
return None;
}
if !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
return None;
}
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
if style & WS_CHILD != 0 {
return None;
}
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
}
if pid == 0 || pid == own_pid {
return None;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return None;
}
let width = (rect.right - rect.left).max(0) as u32;
let height = (rect.bottom - rect.top).max(0) as u32;
if width == 0 || height == 0 {
return None;
}
let title = window_title(hwnd).unwrap_or_else(|| format!("Window {pid}"));
Some(ScreenCaptureSourceDescriptor {
kind: "window".to_string(),
id: format!("window:{}:0", hwnd as usize),
name: title,
width,
height,
target_pid: Some(pid),
})
}
#[cfg(target_os = "windows")]
fn window_title(hwnd: HWND) -> Option<String> {
let len = unsafe { GetWindowTextLengthW(hwnd) };
if len <= 0 {
return None;
}
let mut buf = vec![0u16; (len + 1) as usize];
let copied = unsafe { GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32) };
if copied <= 0 {
return None;
}
let title = String::from_utf16_lossy(&buf[..copied as usize])
.trim()
.to_string();
if title.is_empty() { None } else { Some(title) }
}
@@ -1,183 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::time::{Duration, Instant};
pub const MIN_STALL_THRESHOLD: Duration = Duration::from_millis(250);
pub const MAX_STALL_THRESHOLD: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StallSignal {
Quiet,
Stalled { frames_seen: u64, elapsed_ms: u64 },
Resumed { stalled_for_ms: u64 },
}
pub struct NoFrameStallTracker {
threshold: Duration,
last_frame_at: Instant,
frames_seen: u64,
reported: bool,
}
impl NoFrameStallTracker {
pub fn new(threshold: Duration, started_at: Instant) -> Self {
assert!(
threshold >= MIN_STALL_THRESHOLD,
"stall threshold at least the minimum"
);
assert!(
threshold <= MAX_STALL_THRESHOLD,
"stall threshold at most the maximum"
);
Self {
threshold,
last_frame_at: started_at,
frames_seen: 0,
reported: false,
}
}
pub fn observe(
&mut self,
now: Instant,
produced_frame: bool,
target_expects_frames: bool,
) -> StallSignal {
let idle = now.saturating_duration_since(self.last_frame_at);
if produced_frame {
self.frames_seen = self.frames_seen.saturating_add(1);
self.last_frame_at = now;
if !self.reported {
return StallSignal::Quiet;
}
self.reported = false;
return StallSignal::Resumed {
stalled_for_ms: duration_ms(idle),
};
}
if !target_expects_frames {
self.last_frame_at = now;
return StallSignal::Quiet;
}
if self.reported || idle < self.threshold {
return StallSignal::Quiet;
}
self.reported = true;
StallSignal::Stalled {
frames_seen: self.frames_seen,
elapsed_ms: duration_ms(idle),
}
}
}
fn duration_ms(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
const THRESHOLD: Duration = Duration::from_millis(3000);
fn tracker(start: Instant) -> NoFrameStallTracker {
NoFrameStallTracker::new(THRESHOLD, start)
}
#[test]
fn stays_quiet_before_the_threshold_elapses() {
let start = Instant::now();
let mut stall = tracker(start);
assert_eq!(
stall.observe(start + Duration::from_millis(2999), false, true),
StallSignal::Quiet
);
}
#[test]
fn reports_a_stall_once_when_no_frame_ever_arrives() {
let start = Instant::now();
let mut stall = tracker(start);
assert_eq!(
stall.observe(start + THRESHOLD, false, true),
StallSignal::Stalled {
frames_seen: 0,
elapsed_ms: 3000,
}
);
assert_eq!(
stall.observe(start + Duration::from_millis(9000), false, true),
StallSignal::Quiet,
"a stall episode is reported at most once"
);
}
#[test]
fn counts_delivered_frames_before_a_later_stall() {
let start = Instant::now();
let mut stall = tracker(start);
assert_eq!(
stall.observe(start + Duration::from_millis(16), true, true),
StallSignal::Quiet
);
assert_eq!(
stall.observe(start + Duration::from_millis(32), true, true),
StallSignal::Quiet
);
assert_eq!(
stall.observe(start + Duration::from_millis(3032), false, true),
StallSignal::Stalled {
frames_seen: 2,
elapsed_ms: 3000,
}
);
}
#[test]
fn resuming_frames_clears_the_episode_and_arms_the_next_one() {
let start = Instant::now();
let mut stall = tracker(start);
assert_eq!(
stall.observe(start + THRESHOLD, false, true),
StallSignal::Stalled {
frames_seen: 0,
elapsed_ms: 3000,
}
);
assert_eq!(
stall.observe(start + Duration::from_millis(4000), true, true),
StallSignal::Resumed {
stalled_for_ms: 4000,
}
);
assert_eq!(
stall.observe(start + Duration::from_millis(7000), false, true),
StallSignal::Stalled {
frames_seen: 1,
elapsed_ms: 3000,
}
);
}
#[test]
fn a_target_that_is_not_expected_to_produce_frames_never_stalls() {
let start = Instant::now();
let mut stall = tracker(start);
assert_eq!(
stall.observe(start + Duration::from_millis(60_000), false, false),
StallSignal::Quiet
);
assert_eq!(
stall.observe(start + Duration::from_millis(62_000), false, true),
StallSignal::Quiet,
"the idle window restarts once the target can produce frames again"
);
assert_eq!(
stall.observe(start + Duration::from_millis(63_000), false, true),
StallSignal::Stalled {
frames_seen: 0,
elapsed_ms: 3000,
}
);
}
}
@@ -1,948 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::{Arc, atomic::Ordering};
use windows::Graphics::Capture::{
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession,
};
use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
use windows::Graphics::DirectX::DirectXPixelFormat;
use windows::Graphics::SizeInt32;
use windows::Win32::Foundation::{HWND, LPARAM, RECT, RPC_E_CHANGED_MODE};
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Device,
ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::IDXGIDevice;
use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR};
use windows::Win32::System::WinRT::Direct3D11::{
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
};
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize};
use windows::Win32::UI::WindowsAndMessaging::{IsWindow, IsWindowVisible};
use windows::core::{BOOL, Interface};
use crate::dxgi_capture::{
SharedTextureOutput, capture_timestamp_us, create_shared_output_texture,
pacing_sleep_and_next_deadline, resolve_output_size,
};
use crate::nv12_gpu::Nv12GpuConverter;
use crate::stall::{NoFrameStallTracker, StallSignal};
use crate::{
CaptureInner, emit_lifecycle, emit_shared_texture_frame, note_media_frame_without_sink,
resolve_frame_sink,
};
const WGC_FRAME_POOL_BUFFERS: i32 = 2;
const WGC_FRAME_DRAIN_LIMIT: u32 = 4;
const WGC_MONITOR_ENUM_LIMIT: usize = 16;
const WGC_STALL_THRESHOLD: std::time::Duration = std::time::Duration::from_millis(3000);
fn ensure_winrt_initialized() {
let result = unsafe { RoInitialize(RO_INIT_MULTITHREADED) };
if let Err(error) = result {
assert!(
error.code() == RPC_E_CHANGED_MODE,
"RoInitialize failed unexpectedly: {error:?}"
);
}
}
pub fn wgc_capture_supported() -> bool {
ensure_winrt_initialized();
GraphicsCaptureSession::IsSupported().unwrap_or(false)
}
#[derive(Clone, Copy)]
pub enum WgcCaptureTarget {
Window(HWND),
Monitor(HMONITOR),
}
impl WgcCaptureTarget {
fn create_item(
self,
interop: &IGraphicsCaptureItemInterop,
) -> Result<GraphicsCaptureItem, String> {
match self {
Self::Window(hwnd) => unsafe { interop.CreateForWindow(hwnd) }
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForWindow: {e}")),
Self::Monitor(monitor) => unsafe { interop.CreateForMonitor(monitor) }
.map_err(|e| format!("IGraphicsCaptureItemInterop::CreateForMonitor: {e}")),
}
}
fn is_alive(self) -> bool {
match self {
Self::Window(hwnd) => unsafe { IsWindow(Some(hwnd)) }.as_bool(),
Self::Monitor(monitor) => !monitor.is_invalid(),
}
}
fn expects_frames(self) -> bool {
match self {
Self::Window(hwnd) => unsafe { IsWindowVisible(hwnd) }.as_bool(),
Self::Monitor(monitor) => !monitor.is_invalid(),
}
}
fn label(self) -> &'static str {
match self {
Self::Window(_) => "window",
Self::Monitor(_) => "monitor",
}
}
fn closed_message(self) -> &'static str {
match self {
Self::Window(_) => "window closed",
Self::Monitor(_) => "monitor capture target unavailable",
}
}
}
pub fn parse_monitor_source_id(source_id: &str, source_kind: &str) -> Option<HMONITOR> {
if source_kind != "screen" {
return None;
}
let mut parts = source_id.split(':');
if parts.next()? != "screen" {
return None;
}
let index = parts.next()?.parse::<usize>().ok()?;
let sub_id = parts.next()?;
if sub_id != "0" && sub_id != "1" {
return None;
}
if parts.next().is_some() || index >= WGC_MONITOR_ENUM_LIMIT {
return None;
}
enumerate_monitors().get(index).copied()
}
fn enumerate_monitors() -> Vec<HMONITOR> {
struct MonitorEnumState {
monitors: Vec<HMONITOR>,
}
unsafe extern "system" fn enum_monitor(
monitor: HMONITOR,
_hdc: HDC,
_rect: *mut RECT,
param: LPARAM,
) -> BOOL {
let state = unsafe { &mut *(param.0 as *mut MonitorEnumState) };
if state.monitors.len() >= WGC_MONITOR_ENUM_LIMIT {
return BOOL(0);
}
if !monitor.is_invalid() {
state.monitors.push(monitor);
}
BOOL(1)
}
let mut state = MonitorEnumState {
monitors: Vec::with_capacity(WGC_MONITOR_ENUM_LIMIT),
};
unsafe {
let _ = EnumDisplayMonitors(
None,
None,
Some(enum_monitor),
LPARAM((&mut state as *mut MonitorEnumState) as isize),
);
}
assert!(
state.monitors.len() <= WGC_MONITOR_ENUM_LIMIT,
"monitor enumeration bounded"
);
state.monitors
}
pub struct WgcCaptureSession {
device: ID3D11Device,
context: ID3D11DeviceContext,
d3d_device: IDirect3DDevice,
item: GraphicsCaptureItem,
target: WgcCaptureTarget,
output_width: u32,
output_height: u32,
requested_width: Option<u32>,
requested_height: Option<u32>,
}
unsafe impl Send for WgcCaptureSession {}
impl WgcCaptureSession {
pub fn new(
hwnd: HWND,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
Self::new_for_target(
WgcCaptureTarget::Window(hwnd),
requested_width,
requested_height,
)
}
pub fn new_monitor(
monitor: HMONITOR,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
Self::new_for_target(
WgcCaptureTarget::Monitor(monitor),
requested_width,
requested_height,
)
}
fn new_for_target(
target: WgcCaptureTarget,
requested_width: Option<u32>,
requested_height: Option<u32>,
) -> Result<Self, String> {
ensure_winrt_initialized();
let (device, context) = crate::d3d11_device::create_shared_texture_device(None)?;
let dxgi_device: IDXGIDevice = device
.cast()
.map_err(|e| format!("IDXGIDevice cast: {e}"))?;
let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }
.map_err(|e| format!("CreateDirect3D11DeviceFromDXGIDevice: {e}"))?;
let d3d_device: IDirect3DDevice = inspectable
.cast()
.map_err(|e| format!("IDirect3DDevice cast: {e}"))?;
let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()
.map_err(|e| format!("GraphicsCaptureItem interop factory: {e}"))?;
let item = target.create_item(&interop)?;
let size = item
.Size()
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
let content_width = size.Width.max(1) as u32;
let content_height = size.Height.max(1) as u32;
let (output_width, output_height) = resolve_output_size(
content_width,
content_height,
requested_width,
requested_height,
);
assert!(output_width > 0, "WGC output width positive");
assert!(output_height > 0, "WGC output height positive");
Ok(Self {
device,
context,
d3d_device,
item,
target,
output_width,
output_height,
requested_width,
requested_height,
})
}
pub fn capture_width(&self) -> u32 {
assert!(self.output_width > 0, "WGC output width positive");
assert!(self.output_height > 0, "WGC output height positive");
self.output_width
}
pub fn capture_height(&self) -> u32 {
assert!(self.output_height > 0, "WGC output height positive");
assert!(self.output_width > 0, "WGC output width positive");
self.output_height
}
}
struct WgcState {
frame_pool: Direct3D11CaptureFramePool,
session: GraphicsCaptureSession,
output_pipeline: Option<WgcOutputPipeline>,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
}
impl WgcState {
fn close(self) {
let _ = self.session.Close();
let _ = self.frame_pool.Close();
}
}
struct WgcNv12Pipeline {
input_resource: ID3D11Resource,
converter: Nv12GpuConverter,
}
enum WgcOutputPipeline {
Bgra(SharedTextureOutput),
Nv12(WgcNv12Pipeline),
}
enum WgcFrameResult {
Ok,
NoFrame,
Resized { width: u32, height: u32 },
Error(String),
}
enum LoopStep {
Paced,
Restart,
}
struct WgcLoopContext {
device: ID3D11Device,
context: ID3D11DeviceContext,
d3d_device: IDirect3DDevice,
item: GraphicsCaptureItem,
target: WgcCaptureTarget,
requested_width: Option<u32>,
requested_height: Option<u32>,
}
fn wgc_loop_context(inner: &Arc<CaptureInner>) -> Option<WgcLoopContext> {
let guard = inner.wgc_session.lock();
let session = guard.as_ref()?;
Some(WgcLoopContext {
device: session.device.clone(),
context: session.context.clone(),
d3d_device: session.d3d_device.clone(),
item: session.item.clone(),
target: session.target,
requested_width: session.requested_width,
requested_height: session.requested_height,
})
}
fn teardown_wgc_state(wgc_state: &mut Option<WgcState>) {
if let Some(state) = wgc_state.take() {
state.close();
}
}
fn sleep_with_backoff(backoff: &mut std::time::Duration) {
assert!(
*backoff >= std::time::Duration::from_millis(100),
"backoff at least base interval"
);
assert!(
*backoff <= std::time::Duration::from_secs(2),
"backoff bounded"
);
std::thread::sleep(*backoff);
*backoff = (*backoff * 2).min(std::time::Duration::from_secs(2));
}
pub fn capture_loop(inner: &Arc<CaptureInner>, frame_interval: std::time::Duration) {
ensure_winrt_initialized();
let Some(ctx) = wgc_loop_context(inner) else {
emit_lifecycle(inner, "closed-clean", "no WGC session");
return;
};
let capture_id = inner.capture_id.lock().clone();
let mut wgc_state: Option<WgcState> = None;
let mut recreate_backoff = std::time::Duration::from_millis(100);
let capture_start = std::time::Instant::now();
let mut next_frame_deadline = capture_start + frame_interval;
let mut frames_dropped_coalesced: u64 = 0;
let mut stall_tracker = NoFrameStallTracker::new(WGC_STALL_THRESHOLD, capture_start);
while inner.running.load(Ordering::Acquire) {
if !ctx.target.is_alive() {
emit_lifecycle(inner, "closed", ctx.target.closed_message());
break;
}
if wgc_state.is_none() {
match setup_wgc_state(&ctx) {
Ok(state) => {
wgc_state = Some(state);
}
Err(e) => {
if !inner.running.load(Ordering::Acquire) {
break;
}
emit_lifecycle(
inner,
"error",
&format!("Failed to create WGC capture session: {e}"),
);
sleep_with_backoff(&mut recreate_backoff);
continue;
}
}
}
let Some(state) = wgc_state.as_mut() else {
continue;
};
let result = poll_and_emit_frame(
inner,
&ctx.context,
state,
capture_id.as_deref(),
capture_start,
&mut frames_dropped_coalesced,
);
let signal = stall_tracker.observe(
std::time::Instant::now(),
matches!(result, WgcFrameResult::Ok),
ctx.target.expects_frames(),
);
emit_stall_signal(inner, ctx.target, signal);
match handle_frame_result(inner, &ctx, &mut wgc_state, result, &mut recreate_backoff) {
LoopStep::Paced => {}
LoopStep::Restart => continue,
}
let now = std::time::Instant::now();
let (sleep_duration, deadline) =
pacing_sleep_and_next_deadline(now, next_frame_deadline, frame_interval);
next_frame_deadline = deadline;
if sleep_duration > std::time::Duration::ZERO {
std::thread::sleep(sleep_duration);
}
}
teardown_wgc_state(&mut wgc_state);
inner.running.store(false, Ordering::Release);
emit_lifecycle(inner, "closed-clean", "capture stopped");
}
fn emit_stall_signal(inner: &Arc<CaptureInner>, target: WgcCaptureTarget, signal: StallSignal) {
match signal {
StallSignal::Quiet => {}
StallSignal::Stalled {
frames_seen,
elapsed_ms,
} => {
emit_lifecycle(
inner,
"stalled",
&stall_detail(target, frames_seen, elapsed_ms),
);
}
StallSignal::Resumed { stalled_for_ms } => {
emit_lifecycle(
inner,
"diagnostic",
&format!(
"WGC {} capture frames resumed after {stalled_for_ms}ms without a frame",
target.label()
),
);
}
}
}
fn stall_detail(target: WgcCaptureTarget, frames_seen: u64, elapsed_ms: u64) -> String {
let label = target.label();
if frames_seen == 0 {
return format!(
"WGC {label} capture stalled: the target is still alive but Windows Graphics Capture \
has delivered no frame in the first {elapsed_ms}ms; the stream is blank"
);
}
format!(
"WGC {label} capture stalled: no new frame for {elapsed_ms}ms after {frames_seen} frames; \
the target may be paused, occluded, or no longer rendering"
)
}
fn handle_frame_result(
inner: &Arc<CaptureInner>,
ctx: &WgcLoopContext,
wgc_state: &mut Option<WgcState>,
result: WgcFrameResult,
recreate_backoff: &mut std::time::Duration,
) -> LoopStep {
match result {
WgcFrameResult::Ok => {
*recreate_backoff = std::time::Duration::from_millis(100);
LoopStep::Paced
}
WgcFrameResult::NoFrame => LoopStep::Paced,
WgcFrameResult::Resized { width, height } => {
let Some(state) = wgc_state.as_mut() else {
return LoopStep::Restart;
};
match resize_wgc_state(ctx, state, width, height) {
Ok(()) => LoopStep::Paced,
Err(e) => {
emit_lifecycle(
inner,
"error",
&format!("WGC frame pool resize failed: {e}"),
);
teardown_wgc_state(wgc_state);
sleep_with_backoff(recreate_backoff);
LoopStep::Restart
}
}
}
WgcFrameResult::Error(e) => {
emit_lifecycle(inner, "error", &e);
{
let mut guard = inner.fallback.lock();
if let Some(tracker) = guard.as_mut() {
let _ = tracker.observe(crate::fallback::FailureSignature::DeviceLost);
}
}
teardown_wgc_state(wgc_state);
sleep_with_backoff(recreate_backoff);
LoopStep::Restart
}
}
}
fn setup_wgc_state(ctx: &WgcLoopContext) -> Result<WgcState, String> {
let size = ctx
.item
.Size()
.map_err(|e| format!("GraphicsCaptureItem.Size: {e}"))?;
let content_width = size.Width.max(1) as u32;
let content_height = size.Height.max(1) as u32;
let (out_w, out_h) = resolve_output_size(
content_width,
content_height,
ctx.requested_width,
ctx.requested_height,
);
create_wgc_state_for_format(
ctx,
DirectXPixelFormat::R16G16B16A16Float,
content_width,
content_height,
out_w,
out_h,
)
.or_else(|_| {
create_wgc_state_for_format(
ctx,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
content_width,
content_height,
out_w,
out_h,
)
})
}
fn create_wgc_state_for_format(
ctx: &WgcLoopContext,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
) -> Result<WgcState, String> {
let output_pipeline = create_wgc_output_pipeline(
ctx,
pixel_format,
content_width,
content_height,
out_w,
out_h,
)?;
let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
&ctx.d3d_device,
pixel_format,
WGC_FRAME_POOL_BUFFERS,
SizeInt32 {
Width: content_width as i32,
Height: content_height as i32,
},
)
.map_err(|e| format!("Direct3D11CaptureFramePool::CreateFreeThreaded: {e}"))?;
let session = frame_pool
.CreateCaptureSession(&ctx.item)
.map_err(|e| format!("Direct3D11CaptureFramePool.CreateCaptureSession: {e}"))?;
session
.StartCapture()
.map_err(|e| format!("GraphicsCaptureSession.StartCapture: {e}"))?;
Ok(WgcState {
frame_pool,
session,
output_pipeline,
pixel_format,
content_width,
content_height,
out_w,
out_h,
})
}
fn resize_wgc_state(
ctx: &WgcLoopContext,
state: &mut WgcState,
width: u32,
height: u32,
) -> Result<(), String> {
assert!(width > 0, "WGC resize width positive");
assert!(height > 0, "WGC resize height positive");
let (out_w, out_h) =
resolve_output_size(width, height, ctx.requested_width, ctx.requested_height);
match resize_wgc_state_for_format(ctx, state, state.pixel_format, width, height, out_w, out_h) {
Ok(()) => Ok(()),
Err(first_error) if state.pixel_format == DirectXPixelFormat::R16G16B16A16Float => {
resize_wgc_state_for_format(
ctx,
state,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
width,
height,
out_w,
out_h,
)
.map_err(|fallback_error| {
format!(
"HDR resize failed ({first_error}); BGRA fallback failed ({fallback_error})"
)
})
}
Err(first_error) => Err(first_error),
}
}
fn resize_wgc_state_for_format(
ctx: &WgcLoopContext,
state: &mut WgcState,
pixel_format: DirectXPixelFormat,
width: u32,
height: u32,
out_w: u32,
out_h: u32,
) -> Result<(), String> {
let output_pipeline =
create_wgc_output_pipeline(ctx, pixel_format, width, height, out_w, out_h)?;
state
.frame_pool
.Recreate(
&ctx.d3d_device,
pixel_format,
WGC_FRAME_POOL_BUFFERS,
SizeInt32 {
Width: width as i32,
Height: height as i32,
},
)
.map_err(|e| format!("Direct3D11CaptureFramePool.Recreate: {e}"))?;
state.content_width = width;
state.content_height = height;
state.out_w = out_w;
state.out_h = out_h;
state.pixel_format = pixel_format;
state.output_pipeline = output_pipeline;
Ok(())
}
fn create_wgc_output_pipeline(
ctx: &WgcLoopContext,
pixel_format: DirectXPixelFormat,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
) -> Result<Option<WgcOutputPipeline>, String> {
if pixel_format == DirectXPixelFormat::R16G16B16A16Float {
return create_wgc_nv12_pipeline(
ctx,
DXGI_FORMAT_R16G16B16A16_FLOAT,
content_width,
content_height,
out_w,
out_h,
crate::hdr::SourceFormat::Rgba16Float { hdr: true },
)
.map(WgcOutputPipeline::Nv12)
.map(Some);
}
if out_w != content_width || out_h != content_height {
return create_wgc_nv12_pipeline(
ctx,
DXGI_FORMAT_B8G8R8A8_UNORM,
content_width,
content_height,
out_w,
out_h,
crate::hdr::SourceFormat::Bgra8,
)
.map(WgcOutputPipeline::Nv12)
.map(Some);
}
Ok(
create_shared_output_texture(&ctx.device, content_width, content_height)
.ok()
.map(WgcOutputPipeline::Bgra),
)
}
fn create_wgc_nv12_pipeline(
ctx: &WgcLoopContext,
input_format: DXGI_FORMAT,
content_width: u32,
content_height: u32,
out_w: u32,
out_h: u32,
source_format: crate::hdr::SourceFormat,
) -> Result<WgcNv12Pipeline, String> {
assert!(content_width > 0, "WGC NV12 input width positive");
assert!(content_height > 0, "WGC NV12 input height positive");
let input_desc = D3D11_TEXTURE2D_DESC {
Width: content_width,
Height: content_height,
MipLevels: 1,
ArraySize: 1,
Format: input_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
CPUAccessFlags: 0,
MiscFlags: 0,
};
let mut input_texture = None;
unsafe {
ctx.device
.CreateTexture2D(&input_desc, None, Some(&mut input_texture))
}
.map_err(|e| format!("CreateTexture2D WGC NV12 input: {e}"))?;
let input_texture =
input_texture.ok_or_else(|| "CreateTexture2D WGC NV12 input returned null".to_string())?;
let input_resource: ID3D11Resource = input_texture
.cast()
.map_err(|e| format!("ID3D11Resource WGC NV12 input cast: {e}"))?;
let converter = Nv12GpuConverter::new(
&ctx.device,
&ctx.context,
&input_texture,
content_width,
content_height,
out_w,
out_h,
source_format,
)
.ok_or_else(|| "WGC NV12 converter unavailable".to_string())?;
Ok(WgcNv12Pipeline {
input_resource,
converter,
})
}
fn poll_and_emit_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
state: &mut WgcState,
capture_id: Option<&str>,
capture_start: std::time::Instant,
frames_dropped_coalesced: &mut u64,
) -> WgcFrameResult {
let mut newest: Option<Direct3D11CaptureFrame> = None;
let mut drained: u32 = 0;
while drained < WGC_FRAME_DRAIN_LIMIT {
let Ok(frame) = state.frame_pool.TryGetNextFrame() else {
break;
};
if let Some(previous) = newest.replace(frame) {
*frames_dropped_coalesced += 1;
let _ = previous.Close();
}
drained += 1;
}
assert!(drained <= WGC_FRAME_DRAIN_LIMIT, "frame drain bounded");
let Some(frame) = newest else {
return WgcFrameResult::NoFrame;
};
let result = emit_wgc_frame(inner, context, state, capture_id, &frame, capture_start);
let _ = frame.Close();
result
}
fn wgc_frame_source_resource(frame: &Direct3D11CaptureFrame) -> Result<ID3D11Resource, String> {
let surface = frame
.Surface()
.map_err(|e| format!("Direct3D11CaptureFrame.Surface: {e}"))?;
let access: IDirect3DDxgiInterfaceAccess = surface
.cast()
.map_err(|e| format!("IDirect3DDxgiInterfaceAccess cast: {e}"))?;
let source_texture: ID3D11Texture2D =
unsafe { access.GetInterface() }.map_err(|e| format!("WGC surface GetInterface: {e}"))?;
source_texture
.cast()
.map_err(|e| format!("ID3D11Resource WGC surface cast: {e}"))
}
fn emit_wgc_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
state: &mut WgcState,
capture_id: Option<&str>,
frame: &Direct3D11CaptureFrame,
capture_start: std::time::Instant,
) -> WgcFrameResult {
let content = match frame.ContentSize() {
Ok(size) => size,
Err(e) => {
return WgcFrameResult::Error(format!("Direct3D11CaptureFrame.ContentSize: {e}"));
}
};
let content_width = content.Width.max(1) as u32;
let content_height = content.Height.max(1) as u32;
if content_width != state.content_width || content_height != state.content_height {
return WgcFrameResult::Resized {
width: content_width,
height: content_height,
};
}
let Some(frame_sink) = resolve_frame_sink(inner, capture_id) else {
note_media_frame_without_sink(
inner,
"WGC frame dropped because no native frame sink is registered",
);
return WgcFrameResult::Ok;
};
let source_resource = match wgc_frame_source_resource(frame) {
Ok(resource) => resource,
Err(e) => return WgcFrameResult::Error(e),
};
let timestamp_us = capture_timestamp_us(capture_start);
let Some(output_pipeline) = state.output_pipeline.as_mut() else {
return WgcFrameResult::Error("WGC shared texture output unavailable".into());
};
match output_pipeline {
WgcOutputPipeline::Bgra(shared_output) => emit_wgc_bgra_frame(
inner,
context,
&frame_sink,
shared_output,
&source_resource,
content_width,
content_height,
timestamp_us,
),
WgcOutputPipeline::Nv12(pipeline) => emit_wgc_nv12_frame(
inner,
context,
&frame_sink,
pipeline,
&source_resource,
content_width,
content_height,
timestamp_us,
),
}
}
#[allow(clippy::too_many_arguments)]
fn emit_wgc_bgra_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
frame_sink: &crate::FrameSinkRef,
shared_output: &mut SharedTextureOutput,
source_resource: &ID3D11Resource,
content_width: u32,
content_height: u32,
timestamp_us: i64,
) -> WgcFrameResult {
if shared_output.width != content_width || shared_output.height != content_height {
return WgcFrameResult::Error(
"WGC BGRA shared output size does not match content size".into(),
);
}
let slot_index = shared_output.next_slot_index();
let slot = &shared_output.slots[slot_index];
let output_resource: ID3D11Resource = match slot.texture.cast() {
Ok(resource) => resource,
Err(e) => return WgcFrameResult::Error(format!("ID3D11Resource shared output cast: {e}")),
};
let src_box = D3D11_BOX {
left: 0,
top: 0,
front: 0,
right: content_width,
bottom: content_height,
back: 1,
};
unsafe {
context.CopySubresourceRegion(
&output_resource,
0,
0,
0,
0,
source_resource,
0,
Some(&src_box),
);
context.Flush();
}
let _ = emit_shared_texture_frame(
inner,
frame_sink,
slot.handle,
shared_output.width,
shared_output.height,
shared_output.dxgi_format,
timestamp_us,
);
WgcFrameResult::Ok
}
#[allow(clippy::too_many_arguments)]
fn emit_wgc_nv12_frame(
inner: &Arc<CaptureInner>,
context: &ID3D11DeviceContext,
frame_sink: &crate::FrameSinkRef,
pipeline: &mut WgcNv12Pipeline,
source_resource: &ID3D11Resource,
content_width: u32,
content_height: u32,
timestamp_us: i64,
) -> WgcFrameResult {
let src_box = D3D11_BOX {
left: 0,
top: 0,
front: 0,
right: content_width,
bottom: content_height,
back: 1,
};
unsafe {
context.CopySubresourceRegion(
&pipeline.input_resource,
0,
0,
0,
0,
source_resource,
0,
Some(&src_box),
);
}
let frame = match pipeline.converter.convert_shared_texture() {
Ok(frame) => frame,
Err(error) => return WgcFrameResult::Error(error),
};
let _ = emit_shared_texture_frame(
inner,
frame_sink,
frame.handle,
frame.width,
frame.height,
frame.dxgi_format,
timestamp_us,
);
WgcFrameResult::Ok
}