mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
chore(repo): prune dead code and duplicate abstractions (#3413)
This commit is contained in:
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
|
||||
}
|
||||
Reference in new issue
Block a user