Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
308 lines
9.5 KiB
Rust
308 lines
9.5 KiB
Rust
// 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
|
|
));
|
|
}
|
|
}
|