Add native self-hosted instance connection to fluxer_desktop
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.
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
// 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
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user