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:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
pub const STOLEN_BYTES: usize = 16;
pub const NOP: u32 = 0xD503_201F;
pub const LDR_X16_PC8: u32 = 0x5800_0050;
pub const BR_X16: u32 = 0xD61F_0200;
pub const BLR_X16: u32 = 0xD63F_0200;
pub fn is_b(insn: u32) -> bool {
(insn & 0xFC00_0000) == 0x1400_0000
}
pub fn is_bl(insn: u32) -> bool {
(insn & 0xFC00_0000) == 0x9400_0000
}
pub fn needs_absolute_island(insn: u32) -> bool {
is_b(insn) || is_bl(insn)
}
pub fn branch_target(insn: u32, src_pc: u64) -> Option<u64> {
if !is_b(insn) && !is_bl(insn) {
return None;
}
let imm26 = (insn & 0x03FF_FFFF) as i32;
let off = ((imm26 << 6) >> 6) as i64 * 4;
Some((src_pc as i64 + off) as u64)
}
pub fn encode_imm26(byte_off: i64) -> Option<u32> {
if byte_off & 0b11 != 0 {
return None;
}
let words = byte_off >> 2;
if !(-(1 << 25)..(1 << 25)).contains(&words) {
return None;
}
Some((words as u32) & 0x03FF_FFFF)
}
pub fn append_abs_branch(out: &mut Vec<u8>, addr: u64, link: bool) {
let branch = if link { BLR_X16 } else { BR_X16 };
out.extend_from_slice(&LDR_X16_PC8.to_le_bytes());
out.extend_from_slice(&branch.to_le_bytes());
out.extend_from_slice(&addr.to_le_bytes());
}
pub fn adrp_target(insn: u32, src_pc: u64) -> Option<u64> {
if (insn & 0x9F00_0000) != 0x9000_0000 {
return None;
}
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
let page = (src_pc & !0xFFF) as i64 + imm21 * 4096;
Some(page as u64)
}
fn ldr_unsigned_64(insn: u32) -> Option<(u32, u32, u64)> {
if (insn & 0xFFC0_0000) != 0xF940_0000 {
return None;
}
let imm12 = ((insn >> 10) & 0xFFF) as u64;
let rn = (insn >> 5) & 0x1F;
let rt = insn & 0x1F;
Some((rt, rn, imm12 * 8))
}
fn br_register(insn: u32) -> Option<u32> {
if (insn & 0xFFFF_FC1F) != 0xD61F_0000 {
return None;
}
Some((insn >> 5) & 0x1F)
}
pub unsafe fn import_thunk_target(prologue: &[u8], src_base: u64) -> Option<u64> {
if prologue.len() < 12 {
return None;
}
let adrp = u32::from_le_bytes(prologue[0..4].try_into().ok()?);
let ldr = u32::from_le_bytes(prologue[4..8].try_into().ok()?);
let br = u32::from_le_bytes(prologue[8..12].try_into().ok()?);
let adrp_reg = adrp & 0x1F;
let page = adrp_target(adrp, src_base)?;
let (ldr_rt, ldr_rn, offset) = ldr_unsigned_64(ldr)?;
let br_rn = br_register(br)?;
if adrp_reg != ldr_rn || ldr_rt != br_rn {
return None;
}
let pointer_addr = page.checked_add(offset)?;
let target = unsafe { core::ptr::read_unaligned(pointer_addr as *const u64) };
(target != 0).then_some(target)
}
pub fn emit_branch_to_island(insn: u32, dst_pc: u64, island_addr: u64) -> Option<u32> {
let link = is_bl(insn);
let off = island_addr as i64 - dst_pc as i64;
let imm = encode_imm26(off)?;
let opc = if link { 0x9400_0000 } else { 0x1400_0000 };
Some(opc | imm)
}
pub fn island_for_branch(insn: u32, src_pc: u64) -> Option<Vec<u8>> {
let target = branch_target(insn, src_pc)?;
let mut bytes = Vec::new();
append_abs_branch(&mut bytes, target, is_bl(insn));
Some(bytes)
}
pub fn relocate_instruction(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
if (insn & 0x9F00_0000) == 0x9000_0000 {
return relocate_adr(insn, src_pc, dst_pc, true);
}
if (insn & 0x9F00_0000) == 0x1000_0000 {
return relocate_adr(insn, src_pc, dst_pc, false);
}
if is_b(insn) || is_bl(insn) {
let target = branch_target(insn, src_pc)?;
let off = target as i64 - dst_pc as i64;
let imm = encode_imm26(off)?;
return Some((insn & 0xFC00_0000) | imm);
}
if (insn & 0xFF00_0010) == 0x5400_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
if (insn & 0x7F00_0000) == 0x3400_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
if (insn & 0x7F00_0000) == 0x3600_0000 {
return relocate_tbz(insn, src_pc, dst_pc);
}
if (insn & 0x3B00_0000) == 0x1800_0000 {
return relocate_imm19_at5(insn, src_pc, dst_pc);
}
Some(insn)
}
fn relocate_adr(insn: u32, src_pc: u64, dst_pc: u64, page: bool) -> Option<u32> {
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
let (src_ref, dst_ref, scale) = if page {
(src_pc & !0xFFF, dst_pc & !0xFFF, 4096i64)
} else {
(src_pc, dst_pc, 1i64)
};
let target = src_ref as i64 + imm21 * scale;
let new_off = target - dst_ref as i64;
if scale != 1 && new_off & 0xFFF != 0 {
return None;
}
let scaled = new_off / scale;
if !(-(1 << 20)..(1 << 20)).contains(&scaled) {
return None;
}
let new_raw = (scaled as u32) & 0x1F_FFFF;
let new_immlo = (new_raw & 0x3) << 29;
let new_immhi = ((new_raw >> 2) & 0x7FFFF) << 5;
Some((insn & 0x9F00_001F) | new_immlo | new_immhi)
}
fn relocate_imm19_at5(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
let imm19 = ((insn >> 5) & 0x7FFFF) as i64;
let off = ((imm19 << 45) >> 45) * 4;
let target = src_pc as i64 + off;
let new_off = target - dst_pc as i64;
if new_off & 0b11 != 0 {
return None;
}
let words = new_off >> 2;
if !(-(1 << 18)..(1 << 18)).contains(&words) {
return None;
}
let new_imm19 = ((words as u32) & 0x7FFFF) << 5;
Some((insn & !(0x7FFFF << 5)) | new_imm19)
}
fn relocate_tbz(insn: u32, src_pc: u64, dst_pc: u64) -> Option<u32> {
let imm14 = ((insn >> 5) & 0x3FFF) as i64;
let off = ((imm14 << 50) >> 50) * 4;
let target = src_pc as i64 + off;
let new_off = target - dst_pc as i64;
if new_off & 0b11 != 0 {
return None;
}
let words = new_off >> 2;
if !(-(1 << 13)..(1 << 13)).contains(&words) {
return None;
}
let new_imm14 = ((words as u32) & 0x3FFF) << 5;
Some((insn & !(0x3FFF << 5)) | new_imm14)
}
pub fn assemble_trampoline(
prologue: &[u8],
src_base: u64,
dst_base: u64,
resume: u64,
) -> Option<Vec<u8>> {
if !prologue.len().is_multiple_of(4) {
return None;
}
let count = prologue.len() / 4;
const RETURN_BRANCH_BYTES: usize = 16;
const ISLAND_BYTES: usize = 16;
let islands_base = dst_base + (count * 4) as u64 + RETURN_BRANCH_BYTES as u64;
let mut prologue_out: Vec<u8> = Vec::with_capacity(count * 4);
let mut islands_out: Vec<u8> = Vec::new();
let mut next_island = islands_base;
for i in 0..count {
let insn = u32::from_le_bytes(prologue[i * 4..i * 4 + 4].try_into().ok()?);
let src_pc = src_base + (i * 4) as u64;
let dst_pc = dst_base + (i * 4) as u64;
if needs_absolute_island(insn) {
let island_addr = next_island;
next_island += ISLAND_BYTES as u64;
let relocated = emit_branch_to_island(insn, dst_pc, island_addr)?;
prologue_out.extend_from_slice(&relocated.to_le_bytes());
let island = island_for_branch(insn, src_pc)?;
debug_assert_eq!(island.len(), ISLAND_BYTES);
islands_out.extend_from_slice(&island);
} else {
let relocated = relocate_instruction(insn, src_pc, dst_pc)?;
prologue_out.extend_from_slice(&relocated.to_le_bytes());
}
}
let mut out = prologue_out;
append_abs_branch(&mut out, resume, false);
out.extend_from_slice(&islands_out);
Some(out)
}
pub fn relocated_prologue(prologue: &[u8], src_base: u64, dst_base: u64) -> Option<Vec<u8>> {
let count = prologue.len() / 4;
let body = assemble_trampoline(prologue, src_base, dst_base, src_base + STOLEN_BYTES as u64)?;
Some(body[..count * 4].to_vec())
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,584 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
GAME_CAPTURE_API_OPENGL, GAME_CAPTURE_FALLBACK_NONE,
GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED, HookState, mark_present,
publish_shared_texture_frame, set_capture_flags, set_fallback_reason, verbose_log,
};
use std::{
ffi::c_void,
ptr::null_mut,
sync::atomic::{AtomicBool, Ordering},
};
use windows::{
Win32::{
Foundation::{HMODULE as WinHmodule, HWND as WinHwnd},
Graphics::{
Direct3D::D3D_DRIVER_TYPE_HARDWARE,
Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_RESOURCE_MISC_SHARED, D3D11_SDK_VERSION,
D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDeviceAndSwapChain,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
},
Dxgi::{
Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_MODE_DESC,
DXGI_MODE_SCALING_UNSPECIFIED, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
DXGI_RATIONAL, DXGI_SAMPLE_DESC,
},
DXGI_PRESENT, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD,
DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIResource, IDXGISwapChain,
},
},
},
core::{BOOL as WinBool, Interface},
};
use windows_sys::Win32::{
Foundation::HWND as SysHwnd,
Graphics::OpenGL::{
GL_COLOR_BUFFER_BIT, GL_LINEAR, GL_NEAREST, GL_NO_ERROR, GL_TEXTURE_2D,
GL_TEXTURE_BINDING_2D, glBindTexture, glDeleteTextures, glFinish, glGenTextures,
glGetError, glGetIntegerv, wglGetCurrentContext, wglGetProcAddress,
},
UI::WindowsAndMessaging::DestroyWindow,
};
const WGL_ACCESS_READ_ONLY_NV: u32 = 0x0000;
const WGL_ACCESS_READ_WRITE_NV: u32 = 0x0001;
const WGL_ACCESS_WRITE_DISCARD_NV: u32 = 0x0002;
const GL_READ_FRAMEBUFFER: u32 = 0x8CA8;
const GL_DRAW_FRAMEBUFFER: u32 = 0x8CA9;
const GL_FRAMEBUFFER: u32 = 0x8D40;
const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
const GL_READ_FRAMEBUFFER_BINDING: u32 = 0x8CAA;
const GL_DRAW_FRAMEBUFFER_BINDING: u32 = 0x8CA6;
type DxOpenDeviceNvFn = unsafe extern "system" fn(dx_device: *mut c_void) -> *mut c_void;
type DxCloseDeviceNvFn = unsafe extern "system" fn(device: *mut c_void) -> i32;
type DxRegisterObjectNvFn = unsafe extern "system" fn(
device: *mut c_void,
dx_object: *mut c_void,
name: u32,
object_type: u32,
access: u32,
) -> *mut c_void;
type DxUnregisterObjectNvFn =
unsafe extern "system" fn(device: *mut c_void, object: *mut c_void) -> i32;
type DxLockObjectsNvFn =
unsafe extern "system" fn(device: *mut c_void, count: i32, objects: *const *mut c_void) -> i32;
type DxUnlockObjectsNvFn =
unsafe extern "system" fn(device: *mut c_void, count: i32, objects: *const *mut c_void) -> i32;
type GlGenFramebuffersFn = unsafe extern "system" fn(n: i32, framebuffers: *mut u32);
type GlDeleteFramebuffersFn = unsafe extern "system" fn(n: i32, framebuffers: *const u32);
type GlBindFramebufferFn = unsafe extern "system" fn(target: u32, framebuffer: u32);
type GlFramebufferTexture2DFn = unsafe extern "system" fn(
target: u32,
attachment: u32,
textarget: u32,
texture: u32,
level: i32,
);
type GlCheckFramebufferStatusFn = unsafe extern "system" fn(target: u32) -> u32;
type GlBlitFramebufferFn = unsafe extern "system" fn(
src_x0: i32,
src_y0: i32,
src_x1: i32,
src_y1: i32,
dst_x0: i32,
dst_y0: i32,
dst_x1: i32,
dst_y1: i32,
mask: u32,
filter: u32,
);
struct InteropProcs {
open_device: DxOpenDeviceNvFn,
close_device: DxCloseDeviceNvFn,
register_object: DxRegisterObjectNvFn,
unregister_object: DxUnregisterObjectNvFn,
lock_objects: DxLockObjectsNvFn,
unlock_objects: DxUnlockObjectsNvFn,
gen_framebuffers: GlGenFramebuffersFn,
delete_framebuffers: GlDeleteFramebuffersFn,
bind_framebuffer: GlBindFramebufferFn,
framebuffer_texture_2d: GlFramebufferTexture2DFn,
check_framebuffer_status: GlCheckFramebufferStatusFn,
blit_framebuffer: GlBlitFramebufferFn,
}
pub(crate) struct GlInteropState {
procs: InteropProcs,
_device: ID3D11Device,
_context: ID3D11DeviceContext,
swap_chain: IDXGISwapChain,
_texture: ID3D11Texture2D,
dummy_hwnd: SysHwnd,
shared_handle: u64,
dx_device: *mut c_void,
dx_object: *mut c_void,
gl_texture: u32,
draw_fbo: u32,
width: u32,
height: u32,
}
unsafe impl Send for GlInteropState {}
static GL_GPU_DISABLED: AtomicBool = AtomicBool::new(false);
static GL_GPU_UNAVAILABLE_LOGGED: AtomicBool = AtomicBool::new(false);
static GL_DUMMY_PRESENT_ACTIVE: AtomicBool = AtomicBool::new(false);
struct DummyPresentGuard;
impl DummyPresentGuard {
fn enter() -> Self {
GL_DUMMY_PRESENT_ACTIVE.store(true, Ordering::Release);
Self
}
}
impl Drop for DummyPresentGuard {
fn drop(&mut self) {
GL_DUMMY_PRESENT_ACTIVE.store(false, Ordering::Release);
}
}
fn latch_disable(reason: &str) {
if !GL_GPU_DISABLED.swap(true, Ordering::AcqRel) {
verbose_log(&format!(
"opengl interop: latch-disabling GPU path, falling back to glReadPixels CPU path ({reason})"
));
}
}
pub(crate) fn gpu_path_disabled() -> bool {
GL_GPU_DISABLED.load(Ordering::Acquire)
}
pub(crate) fn dummy_present_active() -> bool {
GL_DUMMY_PRESENT_ACTIVE.load(Ordering::Acquire)
}
unsafe fn load_proc<T>(name: &[u8]) -> Option<T> {
debug_assert_eq!(
name.last(),
Some(&0),
"wglGetProcAddress name must be NUL-terminated"
);
let proc = wglGetProcAddress(name.as_ptr());
match proc {
Some(proc) => Some(std::mem::transmute_copy::<_, T>(&proc)),
None => None,
}
}
impl InteropProcs {
unsafe fn load() -> Option<Self> {
if wglGetCurrentContext().is_null() {
return None;
}
let open_device = load_proc::<DxOpenDeviceNvFn>(b"wglDXOpenDeviceNV\0")?;
let close_device = load_proc::<DxCloseDeviceNvFn>(b"wglDXCloseDeviceNV\0")?;
let register_object = load_proc::<DxRegisterObjectNvFn>(b"wglDXRegisterObjectNV\0")?;
let unregister_object = load_proc::<DxUnregisterObjectNvFn>(b"wglDXUnregisterObjectNV\0")?;
let lock_objects = load_proc::<DxLockObjectsNvFn>(b"wglDXLockObjectsNV\0")?;
let unlock_objects = load_proc::<DxUnlockObjectsNvFn>(b"wglDXUnlockObjectsNV\0")?;
let gen_framebuffers = load_proc::<GlGenFramebuffersFn>(b"glGenFramebuffers\0")?;
let delete_framebuffers = load_proc::<GlDeleteFramebuffersFn>(b"glDeleteFramebuffers\0")?;
let bind_framebuffer = load_proc::<GlBindFramebufferFn>(b"glBindFramebuffer\0")?;
let framebuffer_texture_2d =
load_proc::<GlFramebufferTexture2DFn>(b"glFramebufferTexture2D\0")?;
let check_framebuffer_status =
load_proc::<GlCheckFramebufferStatusFn>(b"glCheckFramebufferStatus\0")?;
let blit_framebuffer = load_proc::<GlBlitFramebufferFn>(b"glBlitFramebuffer\0")?;
Some(Self {
open_device,
close_device,
register_object,
unregister_object,
lock_objects,
unlock_objects,
gen_framebuffers,
delete_framebuffers,
bind_framebuffer,
framebuffer_texture_2d,
check_framebuffer_status,
blit_framebuffer,
})
}
}
unsafe fn create_interop_d3d11_device()
-> Option<(ID3D11Device, ID3D11DeviceContext, IDXGISwapChain, SysHwnd)> {
let dummy_hwnd = crate::create_dummy_window();
if dummy_hwnd.is_null() {
verbose_log("opengl interop: failed to create dummy D3D11 flush window");
return None;
}
let desc = DXGI_SWAP_CHAIN_DESC {
BufferDesc: DXGI_MODE_DESC {
Width: 2,
Height: 2,
RefreshRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
ScanlineOrdering: DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
Scaling: DXGI_MODE_SCALING_UNSPECIFIED,
},
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
OutputWindow: WinHwnd(dummy_hwnd),
Windowed: WinBool(1),
SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
Flags: 0,
};
let mut swap_chain = None;
let mut device = None;
let mut context = None;
let result = D3D11CreateDeviceAndSwapChain(
None,
D3D_DRIVER_TYPE_HARDWARE,
WinHmodule(null_mut()),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&desc),
Some(&mut swap_chain),
Some(&mut device),
None,
Some(&mut context),
);
if result.is_err() {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
match (device, context, swap_chain) {
(Some(device), Some(context), Some(swap_chain)) => {
Some((device, context, swap_chain, dummy_hwnd))
}
_ => {
let _ = DestroyWindow(dummy_hwnd);
None
}
}
}
unsafe fn create_shared_texture(
device: &ID3D11Device,
width: u32,
height: u32,
) -> Option<(ID3D11Texture2D, u64)> {
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut texture = None;
if device
.CreateTexture2D(&desc, None, Some(&mut texture))
.is_err()
{
return None;
}
let texture = texture?;
let handle = texture
.cast::<IDXGIResource>()
.and_then(|resource| resource.GetSharedHandle())
.ok()?;
Some((texture, handle.0 as usize as u64))
}
impl GlInteropState {
unsafe fn create(width: u32, height: u32) -> Option<Self> {
let procs = InteropProcs::load()?;
let (device, context, swap_chain, dummy_hwnd) = create_interop_d3d11_device()?;
let (texture, shared_handle) = match create_shared_texture(&device, width, height) {
Some(texture) => texture,
None => {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
};
if shared_handle == 0 {
let _ = DestroyWindow(dummy_hwnd);
return None;
}
let dx_device = (procs.open_device)(device.as_raw());
if dx_device.is_null() {
verbose_log("opengl interop: wglDXOpenDeviceNV returned NULL");
let _ = DestroyWindow(dummy_hwnd);
return None;
}
verbose_log("opengl interop: wglDXOpenDeviceNV opened private D3D11 device");
let mut gl_texture = 0u32;
glGenTextures(1, &mut gl_texture);
if gl_texture == 0 {
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
let dx_object = (procs.register_object)(
dx_device,
texture.as_raw(),
gl_texture,
GL_TEXTURE_2D,
WGL_ACCESS_WRITE_DISCARD_NV,
);
if dx_object.is_null() {
verbose_log("opengl interop: wglDXRegisterObjectNV returned NULL");
glDeleteTextures(1, &gl_texture);
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
verbose_log(&format!(
"opengl interop: registered D3D11 texture <-> GL texture {gl_texture} ({width}x{height} BGRA)"
));
let mut draw_fbo = 0u32;
(procs.gen_framebuffers)(1, &mut draw_fbo);
if draw_fbo == 0 {
(procs.unregister_object)(dx_device, dx_object);
glDeleteTextures(1, &gl_texture);
(procs.close_device)(dx_device);
let _ = DestroyWindow(dummy_hwnd);
return None;
}
Some(Self {
procs,
_device: device,
_context: context,
swap_chain,
_texture: texture,
dummy_hwnd,
shared_handle,
dx_device,
dx_object,
gl_texture,
draw_fbo,
width,
height,
})
}
fn matches(&self, width: u32, height: u32) -> bool {
self.width == width && self.height == height
}
unsafe fn blit_default_framebuffer(&self) -> bool {
let objects = [self.dx_object];
let mut prev_read_fbo = 0i32;
let mut prev_draw_fbo = 0i32;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &mut prev_read_fbo);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut prev_draw_fbo);
let mut prev_tex = 0i32;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &mut prev_tex);
if (self.procs.lock_objects)(self.dx_device, 1, objects.as_ptr()) == 0 {
verbose_log("opengl interop: wglDXLockObjectsNV FAILED");
return false;
}
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, self.draw_fbo);
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
self.gl_texture,
0,
);
let status = (self.procs.check_framebuffer_status)(GL_DRAW_FRAMEBUFFER);
if status != GL_FRAMEBUFFER_COMPLETE {
verbose_log(&format!(
"opengl interop: draw FBO incomplete (status 0x{status:04X}); unlocking and falling back"
));
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0,
0,
);
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, prev_draw_fbo as u32);
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, prev_read_fbo as u32);
let _ = (self.procs.unlock_objects)(self.dx_device, 1, objects.as_ptr());
return false;
}
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, 0);
let w = self.width as i32;
let h = self.height as i32;
(self.procs.blit_framebuffer)(
0,
0,
w,
h,
0,
h,
w,
0,
GL_COLOR_BUFFER_BIT,
if w == self.width as i32 && h == self.height as i32 {
GL_NEAREST
} else {
GL_LINEAR
},
);
let blit_err = glGetError();
(self.procs.framebuffer_texture_2d)(
GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0,
0,
);
(self.procs.bind_framebuffer)(GL_DRAW_FRAMEBUFFER, prev_draw_fbo as u32);
(self.procs.bind_framebuffer)(GL_READ_FRAMEBUFFER, prev_read_fbo as u32);
glBindTexture(GL_TEXTURE_2D, prev_tex as u32);
glFinish();
if (self.procs.unlock_objects)(self.dx_device, 1, objects.as_ptr()) == 0 {
verbose_log("opengl interop: wglDXUnlockObjectsNV FAILED");
return false;
}
self._context.Flush();
let present_result = {
let _guard = DummyPresentGuard::enter();
self.swap_chain.Present(0, DXGI_PRESENT(0))
};
if present_result.is_err() {
verbose_log(&format!(
"opengl interop: dummy D3D11 Present flush failed hr={:#010x}",
present_result.0 as u32
));
return false;
}
if blit_err != GL_NO_ERROR {
verbose_log(&format!(
"opengl interop: glBlitFramebuffer raised GL error 0x{blit_err:04X}"
));
return false;
}
true
}
}
impl Drop for GlInteropState {
fn drop(&mut self) {
unsafe {
if !self.dx_object.is_null() {
let _ = (self.procs.unregister_object)(self.dx_device, self.dx_object);
}
if self.draw_fbo != 0 {
(self.procs.delete_framebuffers)(1, &self.draw_fbo);
}
if self.gl_texture != 0 {
glDeleteTextures(1, &self.gl_texture);
}
if !self.dx_device.is_null() {
let _ = (self.procs.close_device)(self.dx_device);
}
if !self.dummy_hwnd.is_null() {
let _ = DestroyWindow(self.dummy_hwnd);
}
}
}
}
unsafe fn interop_state_for_frame(
state: &mut HookState,
width: u32,
height: u32,
) -> Option<&mut GlInteropState> {
let recreate = state
.gl_interop
.as_ref()
.map(|interop| !interop.matches(width, height))
.unwrap_or(true);
if recreate {
state.gl_interop = None;
match GlInteropState::create(width, height) {
Some(interop) => state.gl_interop = Some(interop),
None => return None,
}
}
state.gl_interop.as_mut()
}
pub(crate) unsafe fn capture_opengl_frame_gpu(
state: &mut HookState,
hwnd: SysHwnd,
width: u32,
height: u32,
) -> bool {
if gpu_path_disabled() {
return false;
}
let Some(interop) = interop_state_for_frame(state, width, height) else {
if !GL_GPU_UNAVAILABLE_LOGGED.swap(true, Ordering::AcqRel) {
verbose_log(
"opengl interop: WGL_NV_DX_interop2 unavailable or pipeline creation failed",
);
}
latch_disable("interop pipeline creation failed");
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED);
return false;
};
let shared_handle = interop.shared_handle;
let blitted = interop.blit_default_framebuffer();
if !blitted {
latch_disable("lock/blit failed after successful registration");
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED);
return false;
}
mark_present(state, GAME_CAPTURE_API_OPENGL);
set_capture_flags(state, 0);
set_fallback_reason(state, GAME_CAPTURE_FALLBACK_NONE);
let published = publish_shared_texture_frame(
state,
hwnd,
width,
height,
DXGI_FORMAT(DXGI_FORMAT_B8G8R8A8_UNORM.0),
shared_handle,
);
if published {
verbose_log(&format!(
"opengl interop: published shared-texture frame {width}x{height} (handle 0x{shared_handle:X})"
));
}
published
}
@@ -0,0 +1,262 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#[cfg(not(target_arch = "aarch64"))]
use retour::Function;
#[cfg(target_arch = "aarch64")]
pub(crate) use aarch64_function::Function;
#[cfg(target_arch = "aarch64")]
mod aarch64_function {
pub(crate) trait Function: Copy + Sync + 'static {
unsafe fn from_ptr(ptr: *const ()) -> Self;
fn to_ptr(&self) -> *const ();
}
macro_rules! impl_function {
($($arg:ident),*) => {
impl<Ret: 'static, $($arg: 'static),*> Function
for unsafe extern "system" fn($($arg),*) -> Ret
{
unsafe fn from_ptr(ptr: *const ()) -> Self {
core::mem::transmute(ptr)
}
fn to_ptr(&self) -> *const () {
*self as *const ()
}
}
};
}
impl_function!();
impl_function!(A);
impl_function!(A, B);
impl_function!(A, B, C);
impl_function!(A, B, C, D);
impl_function!(A, B, C, D, E);
impl_function!(A, B, C, D, E, F);
}
pub(crate) struct Detour<T: Function> {
inner: Inner<T>,
}
#[cfg(not(target_arch = "aarch64"))]
enum Inner<T: Function> {
Retour(retour::GenericDetour<T>),
}
#[cfg(target_arch = "aarch64")]
enum Inner<T: Function> {
Aarch64(aarch64::Aarch64Detour<T>),
}
impl<T: Function> Detour<T> {
pub(crate) unsafe fn new(target: T, detour: T) -> Result<Self, ()> {
#[cfg(not(target_arch = "aarch64"))]
{
match retour::GenericDetour::<T>::new(target, detour) {
Ok(detour) => Ok(Self {
inner: Inner::Retour(detour),
}),
Err(_) => Err(()),
}
}
#[cfg(target_arch = "aarch64")]
{
aarch64::Aarch64Detour::<T>::new(target, detour).map(|detour| Self {
inner: Inner::Aarch64(detour),
})
}
}
pub(crate) unsafe fn enable(&self) -> Result<(), ()> {
match &self.inner {
#[cfg(not(target_arch = "aarch64"))]
Inner::Retour(detour) => detour.enable().map_err(|_| ()),
#[cfg(target_arch = "aarch64")]
Inner::Aarch64(detour) => detour.enable(),
}
}
pub(crate) fn trampoline_fn(&self) -> T {
match &self.inner {
#[cfg(not(target_arch = "aarch64"))]
Inner::Retour(detour) => unsafe {
T::from_ptr(detour.trampoline() as *const () as *const ())
},
#[cfg(target_arch = "aarch64")]
Inner::Aarch64(detour) => detour.trampoline_fn(),
}
}
}
#[cfg(target_arch = "aarch64")]
mod aarch64 {
use super::Function;
use crate::arm64_reloc::{
NOP, STOLEN_BYTES, append_abs_branch, assemble_trampoline, import_thunk_target,
};
use core::marker::PhantomData;
use std::ptr;
use windows_sys::Win32::System::{
Diagnostics::Debug::FlushInstructionCache,
Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
PAGE_PROTECTION_FLAGS, VirtualAlloc, VirtualFree, VirtualProtect,
},
Threading::GetCurrentProcess,
};
const TRAMPOLINE_CAP: usize = 256;
pub(super) struct Aarch64Detour<T: Function> {
target: *mut u8,
detour: *const u8,
trampoline: *mut u8,
original_prologue: [u8; STOLEN_BYTES],
enabled: std::cell::Cell<bool>,
_marker: PhantomData<T>,
}
unsafe impl<T: Function> Send for Aarch64Detour<T> {}
unsafe impl<T: Function> Sync for Aarch64Detour<T> {}
impl<T: Function> Aarch64Detour<T> {
pub(super) unsafe fn new(target: T, detour: T) -> Result<Self, ()> {
let target_ptr = target.to_ptr() as *mut u8;
let detour_ptr = detour.to_ptr() as *const u8;
if target_ptr.is_null() || detour_ptr.is_null() {
return Err(());
}
let mut original = [0u8; STOLEN_BYTES];
ptr::copy_nonoverlapping(target_ptr, original.as_mut_ptr(), STOLEN_BYTES);
let trampoline = VirtualAlloc(
ptr::null(),
TRAMPOLINE_CAP,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE,
) as *mut u8;
if trampoline.is_null() {
return Err(());
}
let trampoline_addr = trampoline as u64;
let resume = target_ptr as u64 + STOLEN_BYTES as u64;
let body =
match assemble_trampoline(&original, target_ptr as u64, trampoline_addr, resume) {
Some(body) => body,
None => match import_thunk_target(&original, target_ptr as u64) {
Some(target) => {
let mut body = Vec::new();
append_abs_branch(&mut body, target, false);
body
}
None => {
VirtualFree(trampoline.cast(), 0, MEM_RELEASE);
return Err(());
}
},
};
if body.len() > TRAMPOLINE_CAP {
VirtualFree(trampoline.cast(), 0, MEM_RELEASE);
return Err(());
}
ptr::copy_nonoverlapping(body.as_ptr(), trampoline, body.len());
let mut old = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(
trampoline.cast(),
TRAMPOLINE_CAP,
PAGE_EXECUTE_READ,
&mut old,
);
FlushInstructionCache(GetCurrentProcess(), trampoline.cast(), TRAMPOLINE_CAP);
Ok(Self {
target: target_ptr,
detour: detour_ptr,
trampoline,
original_prologue: original,
enabled: std::cell::Cell::new(false),
_marker: PhantomData,
})
}
pub(super) unsafe fn enable(&self) -> Result<(), ()> {
if self.enabled.get() {
return Ok(());
}
let mut patch = Vec::new();
append_abs_branch(&mut patch, self.detour as u64, false);
if patch.len() > STOLEN_BYTES {
return Err(());
}
while patch.len() < STOLEN_BYTES {
patch.extend_from_slice(&NOP.to_le_bytes());
}
let mut old = 0 as PAGE_PROTECTION_FLAGS;
if VirtualProtect(
self.target.cast(),
STOLEN_BYTES,
PAGE_EXECUTE_READWRITE,
&mut old,
) == 0
{
return Err(());
}
ptr::copy_nonoverlapping(patch.as_ptr(), self.target, STOLEN_BYTES);
let mut restore = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(self.target.cast(), STOLEN_BYTES, old, &mut restore);
FlushInstructionCache(GetCurrentProcess(), self.target.cast(), STOLEN_BYTES);
self.enabled.set(true);
Ok(())
}
unsafe fn disable(&self) {
if !self.enabled.get() {
return;
}
let mut old = 0 as PAGE_PROTECTION_FLAGS;
if VirtualProtect(
self.target.cast(),
STOLEN_BYTES,
PAGE_EXECUTE_READWRITE,
&mut old,
) != 0
{
ptr::copy_nonoverlapping(
self.original_prologue.as_ptr(),
self.target,
STOLEN_BYTES,
);
let mut restore = 0 as PAGE_PROTECTION_FLAGS;
VirtualProtect(self.target.cast(), STOLEN_BYTES, old, &mut restore);
FlushInstructionCache(GetCurrentProcess(), self.target.cast(), STOLEN_BYTES);
}
self.enabled.set(false);
}
pub(super) fn trampoline_fn(&self) -> T {
unsafe { T::from_ptr(self.trampoline as *const ()) }
}
}
impl<T: Function> Drop for Aarch64Detour<T> {
fn drop(&mut self) {
unsafe {
self.disable();
if !self.trampoline.is_null() {
VirtualFree(self.trampoline.cast(), 0, MEM_RELEASE);
self.trampoline = ptr::null_mut();
}
}
}
}
}
File diff suppressed because it is too large Load Diff