fix(host/windows): install the GPU-preference hook at process start (before any DXGI)

The win32u hook only works if it patches before DXGI caches the hybrid preference.
It was installed in DuplCapturer::open (first capture), but the SudoVDA
render-adapter selection creates a DXGI factory during virtual-display setup —
seconds earlier — so the preference was already cached and the hook had no effect
(churn persisted; log showed "render adapter chosen" at :02, "hook installed" at
:04). Call install_gpu_pref_hook() at the top of real_main(), before any command
runs, so it beats the first DXGI factory. (open() still calls it too; Once makes
the earliest call win.) Also fix the cosmetic function-cast-as-integer warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 12:39:50 +00:00
parent a01f8a2f58
commit 7cfeddc770
2 changed files with 9 additions and 2 deletions
+2 -2
View File
@@ -223,7 +223,7 @@ unsafe extern "system" fn hybrid_query_hook(gpu_preference: *mut u32) -> i32 {
/// a cached preference of UNSPECIFIED makes DXGI skip the resolution, so the output is NOT reparented /// a cached preference of UNSPECIFIED makes DXGI skip the resolution, so the output is NOT reparented
/// and DDA stays stable on one adapter (this is what makes Apollo's DDA work on this hardware). /// and DDA stays stable on one adapter (this is what makes Apollo's DDA work on this hardware).
/// Installed once, before the first DXGI factory/enumeration; lasts the process lifetime (like Apollo). /// Installed once, before the first DXGI factory/enumeration; lasts the process lifetime (like Apollo).
fn install_gpu_pref_hook() { pub(crate) fn install_gpu_pref_hook() {
use std::sync::Once; use std::sync::Once;
static HOOK: Once = Once::new(); static HOOK: Once = Once::new();
HOOK.call_once(|| unsafe { HOOK.call_once(|| unsafe {
@@ -242,7 +242,7 @@ fn install_gpu_pref_hook() {
let target = target as usize as *mut u8; let target = target as usize as *mut u8;
// x64 absolute jump to our replacement: `mov rax, imm64 ; jmp rax` (12 bytes). We never call the // x64 absolute jump to our replacement: `mov rax, imm64 ; jmp rax` (12 bytes). We never call the
// original, so no trampoline/relocation (hence no detour crate / C length-disassembler dep). // original, so no trampoline/relocation (hence no detour crate / C length-disassembler dep).
let hook = hybrid_query_hook as usize; let hook = hybrid_query_hook as *const () as usize;
let mut patch = [0u8; 12]; let mut patch = [0u8; 12];
patch[0] = 0x48; patch[0] = 0x48;
patch[1] = 0xB8; // mov rax, imm64 patch[1] = 0xB8; // mov rax, imm64
+7
View File
@@ -75,6 +75,13 @@ fn real_main() -> Result<()> {
punktfunk_core::ABI_VERSION punktfunk_core::ABI_VERSION
); );
// Install Apollo's win32u GPU-preference hook BEFORE anything touches DXGI (the SudoVDA
// render-adapter selection creates a DXGI factory during virtual-display setup, well before
// capture). On a hybrid-GPU box this stops DXGI from reparenting the virtual output off the
// capture GPU — the ACCESS_LOST churn fix. Idempotent (Once); harmless on non-hybrid boxes.
#[cfg(target_os = "windows")]
crate::capture::dxgi::install_gpu_pref_hook();
match args.first().map(String::as_str) { match args.first().map(String::as_str) {
// GameStream host control plane (P1.1: mDNS + serverinfo) + management API, and (with // GameStream host control plane (P1.1: mDNS + serverinfo) + management API, and (with
// --native) the native punktfunk/1 host in the same process — the unified host. // --native) the native punktfunk/1 host in the same process — the unified host.