fix(desktop): drop game capture injection and pin dll search (#2069)

This commit is contained in:
Hampus
2026-08-29 14:26:28 +02:00
committed by GitHub
parent 9ad026b8ce
commit 9d0be1ebd1
43 changed files with 158 additions and 12089 deletions
@@ -278,10 +278,6 @@ const DEVELOPER_OPTION_DESCRIPTOR = msg({
message: 'Developer option',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const GAME_CAPTURE_INJECTION_METHOD_DESCRIPTOR = msg({
message: 'Game capture injection (Windows)',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const DEVELOPER_OPTION_LABEL_FALLBACKS: Partial<Record<keyof DeveloperOptionsState, MessageDescriptor>> = {
mockAttachmentStates: ATTACHMENT_MOCKS_DESCRIPTOR,
};
@@ -327,7 +323,6 @@ const formatDeveloperOptionValue = <K extends keyof DeveloperOptionsState>(
case 'mockRequiredActionsResendOutcome':
case 'mockTitlebarPlatformOverride':
case 'mockUpdaterState':
case 'gameCaptureInjectionMethod':
return String(value).replace(/_/g, ' ');
case 'premiumSinceOverride':
case 'premiumUntilOverride':
@@ -410,8 +405,6 @@ export const getDeveloperOptionLabel = (key: keyof DeveloperOptionsState): Messa
return VANITY_URL_DISCLAIMER_DESCRIPTOR;
case 'forceShowVoiceConnection':
return VOICE_CONNECTION_DEBUG_DESCRIPTOR;
case 'gameCaptureInjectionMethod':
return GAME_CAPTURE_INJECTION_METHOD_DESCRIPTOR;
case 'premiumTypeOverride':
return PREMIUM_TYPE_DESCRIPTOR;
case 'premiumLifetimeSequenceOverride':
@@ -1,15 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {getDeveloperOptionLabel} from '@app/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels';
import {DeveloperOptionRadioSubmenu} from '@app/features/channel/components/channel_header_components/developer_tools/DeveloperToolsMenuComponents';
import {translateDescriptor} from '@app/features/channel/components/channel_header_components/developer_tools/DeveloperToolsShared';
import {getGameCaptureInjectionMethodOptions} from '@app/features/channel/components/channel_header_components/developer_tools/OptionPresets';
import {getToggleGroups, type ToggleGroup} from '@app/features/devtools/components/DeveloperOptionsToggleGroups';
import type {DeveloperOptionsState} from '@app/features/devtools/state/DeveloperOptions';
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
import {MenuItemSubmenu} from '@app/features/ui/action_menu/MenuItemSubmenu';
import {getNativePlatformSync} from '@app/features/ui/utils/NativeUtils';
import * as UserSettingsCommands from '@app/features/user/commands/UserSettingsCommands';
import UserSettings from '@app/features/user/state/UserSettings';
import Users from '@app/features/user/state/Users';
@@ -71,7 +67,6 @@ export const GeneralDeveloperOptionsMenu: React.FC = observer(() => {
const currentUser = Users.currentUser;
const canConfigureMentionSuppression = currentUser?.isStaff() ?? false;
const suppressUnprivilegedSelfMentions = UserSettings.getSuppressUnprivilegedSelfMentions();
const isWindows = getNativePlatformSync() === 'windows';
return (
<>
{toggleGroups.map((group, index) => (
@@ -81,14 +76,6 @@ export const GeneralDeveloperOptionsMenu: React.FC = observer(() => {
data-flx="channel.channel-header-components.developer-tools-context-menu.general-developer-options-menu.toggle-group-submenu"
/>
))}
{isWindows && (
<DeveloperOptionRadioSubmenu
label={translateDescriptor(i18n, getDeveloperOptionLabel('gameCaptureInjectionMethod'))}
optionKey="gameCaptureInjectionMethod"
options={getGameCaptureInjectionMethodOptions()}
data-flx="channel.channel-header-components.developer-tools-context-menu.general-developer-options-menu.game-capture-injection"
/>
)}
{canConfigureMentionSuppression && (
<MenuItemSubmenu
label={i18n._(MENTION_CONTROLS_DESCRIPTOR)}
@@ -196,14 +196,3 @@ export const getGiftDurationOptions = (): Array<RadioMenuOption<DeveloperOptions
{value: 12, label: MESSAGE_12_MONTHS_1_YEAR_DESCRIPTOR},
{value: 0, label: LIFETIME_DESCRIPTOR},
];
export const INJECT_METHOD_AUTOMATIC_DESCRIPTOR = msg({
message: 'Automatic',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
export const getGameCaptureInjectionMethodOptions = (): Array<
RadioMenuOption<DeveloperOptionsState['gameCaptureInjectionMethod']>
> => [
{value: 'auto', label: INJECT_METHOD_AUTOMATIC_DESCRIPTOR},
{value: 'remote-thread', label: 'CreateRemoteThread'},
{value: 'set-windows-hook', label: 'SetWindowsHookEx'},
];
@@ -69,7 +69,6 @@ export const DEFAULT_DEVELOPER_OPTIONS = {
mockTitlebarPlatformOverride: 'auto',
mockAttachmentStates: {},
noOpInAppReports: false,
gameCaptureInjectionMethod: 'auto',
} satisfies DeveloperOptionsState;
const PREMIUM_SCENARIO_OVERRIDE_KEYS = new Set<keyof DeveloperOptionsState>([
'premiumTypeOverride',
@@ -97,7 +97,6 @@ export type DeveloperOptionsState = Readonly<{
}
>;
noOpInAppReports: boolean;
gameCaptureInjectionMethod: 'auto' | 'remote-thread' | 'set-windows-hook';
}>;
type MutableDeveloperOptionsState = {
-readonly [K in keyof DeveloperOptionsState]: DeveloperOptionsState[K];
@@ -182,7 +181,6 @@ class DeveloperOptions implements DeveloperOptionsState {
mockGiftRedeemed: boolean | null = null;
mockTitlebarPlatformOverride: DeveloperOptionsState['mockTitlebarPlatformOverride'] = 'auto';
noOpInAppReports = false;
gameCaptureInjectionMethod: DeveloperOptionsState['gameCaptureInjectionMethod'] = 'auto';
constructor() {
makeAutoObservable(this, {}, {autoBind: true});
@@ -255,7 +253,6 @@ class DeveloperOptions implements DeveloperOptionsState {
'mockTitlebarPlatformOverride',
'mockAttachmentStates',
'noOpInAppReports',
'gameCaptureInjectionMethod',
]);
}
@@ -188,7 +188,7 @@ function inferWindowsCaptureMethod(
return null;
}
if (diagnostics?.activeStrategy) return diagnostics.activeStrategy;
if (capture.sourceKind === 'game') return 'game-hook';
if (capture.sourceKind === 'game') return 'wgc';
if (capture.sourceKind === 'screen') return 'wgc';
if (capture.sourceKind === 'window') return 'dxgi-duplication';
return 'native-screen-capture';
+1 -6
View File
@@ -695,8 +695,6 @@ export interface NativeScreenCaptureSource {
targetPid?: number;
}
export type GameCaptureInjectionMethod = 'auto' | 'remote-thread' | 'set-windows-hook';
export interface NativeScreenCaptureRect {
x: number;
y: number;
@@ -710,7 +708,6 @@ export interface NativeScreenCaptureStartOptions {
width?: number;
height?: number;
frameRate?: number;
injectionMethod?: GameCaptureInjectionMethod;
captureId?: string;
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
@@ -746,7 +743,7 @@ export interface NativeScreenCaptureLifecycleMessage {
source?: NativeScreenCaptureLifecycleSource;
}
export type NativeScreenCaptureStrategy = 'game-hook' | 'dxgi-duplication' | 'window-gdi' | string;
export type NativeScreenCaptureStrategy = 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export interface NativeScreenCaptureDiagnostics {
state?: number;
@@ -761,8 +758,6 @@ export interface NativeScreenCaptureDiagnostics {
droppedFrameCounter?: number;
lastPresentTimestampUs?: number;
lastError?: number;
requestedInjectionMethod?: string;
injectionMethod?: string;
activeStrategy?: NativeScreenCaptureStrategy;
lastFallbackReason?: string;
backend?: string;
@@ -359,10 +359,6 @@ function windowsGameCaptureArtifactExcludes(arch) {
];
const excludedNodeArchs = [...supportedTargetArchs, 'ia32'].filter((candidate) => candidate !== arch);
return packageRoots.flatMap((packageRoot) => [
`!${packageRoot}/compatibility.json`,
`!${packageRoot}/fluxer-game-hook.*`,
`!${packageRoot}/fluxer-inject-helper.*`,
`!${packageRoot}/fluxer-vulkan-layer.*`,
...excludedNodeArchs.map((excludedArch) => `!${packageRoot}/win-game-capture.win32-${excludedArch}-msvc.node`),
]);
}
+2 -9
View File
@@ -17,14 +17,7 @@
# own `[workspace]`.
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=/DEPENDENTLOADFLAG:0x800"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
# 32-bit injected game-capture hook/layer (for capturing 32-bit games from the
# 64-bit app). Same rationale: the DLL is LoadLibrary'd into an arbitrary game
# process that may not have the 32-bit VC++ redistributable, so the runtime
# must be static.
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=/DEPENDENTLOADFLAG:0x800"]
@@ -8,10 +8,6 @@ publish = false
[workspace]
resolver = "2"
[features]
default = []
game-capture-hook = []
[lib]
crate-type = ["cdylib", "rlib"]
@@ -1,6 +0,0 @@
{
"$comment": "Optional per-process game-capture injection policy override. Placed next to the native addon, it augments the deny/force-cpu lists compiled into the addon. Entries are matched case-insensitively by executable file name (e.g. game.exe); paths are reduced to their final component. 'deny' refuses injection; 'allow' opts a process back in past the built-in deny list; 'forceCpu' keeps injection but prefers CPU readback over the shared-texture fast path. A missing or malformed file falls back to the embedded defaults.",
"deny": [],
"allow": [],
"forceCpu": []
}
-375
View File
@@ -1,375 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "fluxer_game_hook"
version = "0.0.0"
dependencies = [
"retour",
"windows",
"windows-sys 0.61.2",
]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "iced-x86"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
dependencies = [
"lazy_static",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "mmap-fixed-fixed"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0681853891801e4763dc252e843672faf32bcfee27a0aa3b19733902af450acc"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "region"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
dependencies = [
"bitflags",
"libc",
"mach2",
"windows-sys 0.52.0",
]
[[package]]
name = "retour"
version = "0.4.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ead4bc8e12d553ff70769c5f5c21f5f4f0e73c0018068a6bb5a3d7d3b9e57ec7"
dependencies = [
"cfg-if",
"generic-array",
"iced-x86",
"libc",
"mmap-fixed-fixed",
"once_cell",
"region",
"slice-pool2",
]
[[package]]
name = "slice-pool2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3d689654af89bdfeba29a914ab6ac0236d382eb3b764f7454dde052f2821f8"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
@@ -1,46 +0,0 @@
[package]
name = "fluxer_game_hook"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib"]
# `retour` only ships x86/x86_64 trampoline + patcher backends (its `arch`
# module has no aarch64 variant), so it does not even compile for
# aarch64-pc-windows-msvc. Gate it to the architectures it supports; the
# aarch64 hook uses the in-crate `inline_hook::aarch64` backend instead.
[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))'.dependencies]
retour = "0.4.0-alpha.4"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D9",
"Win32_Graphics_Direct3D10",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D11on12",
"Win32_Graphics_Direct3D12",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
]}
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Graphics_OpenGL",
"Win32_Security",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
]}
@@ -1,248 +0,0 @@
// 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
@@ -1,584 +0,0 @@
// 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
}
@@ -1,262 +0,0 @@
// 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
@@ -1,412 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[path = "../src/arm64_reloc.rs"]
mod arm64_reloc;
use arm64_reloc::*;
fn adr(rd: u32, imm21: i32) -> u32 {
let raw = (imm21 as u32) & 0x1F_FFFF;
let immlo = (raw & 0x3) << 29;
let immhi = ((raw >> 2) & 0x7FFFF) << 5;
0x1000_0000 | immlo | immhi | (rd & 0x1F)
}
fn adrp(rd: u32, imm21: i32) -> u32 {
let raw = (imm21 as u32) & 0x1F_FFFF;
let immlo = (raw & 0x3) << 29;
let immhi = ((raw >> 2) & 0x7FFFF) << 5;
0x9000_0000 | immlo | immhi | (rd & 0x1F)
}
fn b(off_words: i32) -> u32 {
0x1400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bl(off_words: i32) -> u32 {
0x9400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bcond(cond: u32, off_words: i32) -> u32 {
0x5400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (cond & 0xF)
}
fn cbz(rt: u32, off_words: i32) -> u32 {
0xB400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}
fn tbz(rt: u32, bit: u32, off_words: i32) -> u32 {
let b5 = (bit & 0x20) << (31 - 5);
let b40 = (bit & 0x1F) << 19;
0x3600_0000 | b5 | b40 | (((off_words as u32) & 0x3FFF) << 5) | (rt & 0x1F)
}
fn ldr_lit(rt: u32, off_words: i32) -> u32 {
0x5800_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}
fn adr_target(insn: u32, pc: u64, page: bool) -> u64 {
let immlo = ((insn >> 29) & 0x3) as i64;
let immhi = ((insn >> 5) & 0x7FFFF) as i64;
let raw = (immhi << 2) | immlo;
let imm21 = (raw << 43) >> 43;
if page {
((pc & !0xFFF) as i64 + imm21 * 4096) as u64
} else {
(pc as i64 + imm21) as u64
}
}
fn imm19_target(insn: u32, pc: u64) -> u64 {
let imm19 = ((insn >> 5) & 0x7FFFF) as i64;
let off = ((imm19 << 45) >> 45) * 4;
(pc as i64 + off) as u64
}
fn imm14_target(insn: u32, pc: u64) -> u64 {
let imm14 = ((insn >> 5) & 0x3FFF) as i64;
let off = ((imm14 << 50) >> 50) * 4;
(pc as i64 + off) as u64
}
#[test]
fn non_pc_relative_copied_verbatim() {
let stp = 0xA9BF_7BFD;
assert_eq!(relocate_instruction(stp, 0x1000, 0x9000), Some(stp));
let mov = 0xAA01_03E0;
assert_eq!(relocate_instruction(mov, 0x1000, 0x9000), Some(mov));
let sub = 0xD100_83FF;
assert_eq!(relocate_instruction(sub, 0x1000, 0x9000), Some(sub));
let mov_fp_sp = 0x9100_03FD;
assert_eq!(
relocate_instruction(mov_fp_sp, 0x1000, 0x9000),
Some(mov_fp_sp)
);
}
#[test]
fn adr_relocates_to_same_target() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0010_8000u64;
let insn = adr(0, 0x4000);
let original = adr_target(insn, src_pc, false);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, false), original);
assert_eq!(reloc & 0x1F, 0);
}
#[test]
fn adr_negative_offset() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0010_0010u64;
let insn = adr(5, -0x100);
let original = adr_target(insn, src_pc, false);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, false), original);
assert_eq!(reloc & 0x1F, 5);
}
#[test]
fn adr_out_of_range_refused() {
let src_pc = 0x0000_0000_0000u64;
let dst_pc = 0x0000_0080_0000u64;
let insn = adr(0, 0x1000);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn adrp_relocates_to_same_page() {
let src_pc = 0x140_0010_0000u64;
let dst_pc = 0x140_0030_0000u64;
let insn = adrp(9, 0x10);
let original = adr_target(insn, src_pc, true);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, true), original);
assert_eq!(reloc & 0x1F, 9);
assert_eq!(reloc & 0x9F00_0000, 0x9000_0000);
}
#[test]
fn adrp_negative() {
let src_pc = 0x140_0090_0000u64;
let dst_pc = 0x140_0050_0000u64;
let insn = adrp(1, -0x20);
let original = adr_target(insn, src_pc, true);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(adr_target(reloc, dst_pc, true), original);
}
#[test]
fn bcond_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x12_0000u64;
let insn = bcond(0x0, 0x40);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0xF, 0x0);
}
#[test]
fn bcond_out_of_range_refused() {
let src_pc = 0x0u64;
let dst_pc = 0x20_0000u64;
let insn = bcond(0x1, 0x10);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn cbz_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x10_8000u64;
let insn = cbz(3, -0x20);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 3);
assert_eq!(reloc & 0x8000_0000, 0x8000_0000);
}
#[test]
fn tbz_relocates() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x10_1000u64;
let insn = tbz(7, 5, 0x10);
let original = imm14_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm14_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 7);
}
#[test]
fn tbz_out_of_range_refused() {
let src_pc = 0x0u64;
let dst_pc = 0x1_0000u64;
let insn = tbz(0, 1, 0x8);
assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}
#[test]
fn ldr_literal_relocates() {
let src_pc = 0x20_0000u64;
let dst_pc = 0x20_4000u64;
let insn = ldr_lit(2, 0x100);
let original = imm19_target(insn, src_pc);
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(imm19_target(reloc, dst_pc), original);
assert_eq!(reloc & 0x1F, 2);
}
#[test]
fn direct_b_relocation_in_range() {
let src_pc = 0x10_0000u64;
let dst_pc = 0x14_0000u64;
let insn = b(0x100);
let target = branch_target(insn, src_pc).unwrap();
let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
assert_eq!(branch_target(reloc, dst_pc).unwrap(), target);
}
#[test]
fn branch_target_decode() {
let pc = 0x10_0000u64;
assert_eq!(branch_target(b(4), pc), Some(pc + 16));
assert_eq!(branch_target(b(-4), pc), Some(pc - 16));
assert_eq!(branch_target(bl(1), pc), Some(pc + 4));
}
#[test]
fn abs_branch_encoding() {
let mut bytes = Vec::new();
append_abs_branch(&mut bytes, 0x1234_5678_9ABC_DEF0, false);
assert_eq!(bytes.len(), 16);
assert_eq!(
u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), BR_X16);
assert_eq!(
u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
0x1234_5678_9ABC_DEF0
);
let mut linked = Vec::new();
append_abs_branch(&mut linked, 0xDEAD_BEEF, true);
assert_eq!(
u32::from_le_bytes(linked[4..8].try_into().unwrap()),
BLR_X16
);
}
#[test]
fn classify_branches() {
assert!(is_b(b(1)));
assert!(!is_bl(b(1)));
assert!(is_bl(bl(1)));
assert!(!is_b(bl(1)));
assert!(needs_absolute_island(b(1)));
assert!(needs_absolute_island(bl(1)));
assert!(!needs_absolute_island(adr(0, 1)));
assert!(!needs_absolute_island(NOP));
}
#[test]
fn assemble_trampoline_relocates_prologue() {
let stp = 0xA9BF_7BFDu32;
let mov = 0x9100_03FDu32;
let adrp_insn = adrp(8, 0x20);
let mut prologue = Vec::new();
for insn in [stp, mov, adrp_insn, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x140_0030_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("relocatable");
assert_eq!(body.len(), 32);
assert_eq!(u32::from_le_bytes(body[0..4].try_into().unwrap()), stp);
assert_eq!(u32::from_le_bytes(body[4..8].try_into().unwrap()), mov);
assert_eq!(u32::from_le_bytes(body[12..16].try_into().unwrap()), NOP);
let orig_target = adr_target(adrp_insn, src_base + 8, true);
let reloc_adrp = u32::from_le_bytes(body[8..12].try_into().unwrap());
assert_eq!(adr_target(reloc_adrp, dst_base + 8, true), orig_target);
assert_eq!(
u32::from_le_bytes(body[16..20].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(body[20..24].try_into().unwrap()), BR_X16);
assert_eq!(u64::from_le_bytes(body[24..32].try_into().unwrap()), resume);
}
#[test]
fn assemble_trampoline_promotes_leading_branch() {
let lead_b = b(0x4000);
let mut prologue = Vec::new();
for insn in [lead_b, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
assert_eq!(body.len(), 48);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
assert!(is_b(first));
let island_addr = branch_target(first, dst_base).unwrap();
assert_eq!(island_addr, dst_base + 16 + 16);
let original_b_target = branch_target(lead_b, src_base).unwrap();
assert_eq!(
u32::from_le_bytes(body[32..36].try_into().unwrap()),
LDR_X16_PC8
);
assert_eq!(u32::from_le_bytes(body[36..40].try_into().unwrap()), BR_X16);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
original_b_target
);
}
#[test]
fn assemble_trampoline_promotes_leading_bl_with_blr() {
let lead_bl = bl(0x100);
let mut prologue = Vec::new();
for insn in [lead_bl, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
assert_eq!(body.len(), 48);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
assert!(is_bl(first));
let original = branch_target(lead_bl, src_base).unwrap();
assert_eq!(
u32::from_le_bytes(body[36..40].try_into().unwrap()),
BLR_X16
);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
original
);
}
#[test]
fn assemble_trampoline_two_islands() {
let b0 = b(0x10);
let bl1 = bl(0x20);
let mut prologue = Vec::new();
for insn in [b0, NOP, bl1, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x0000_7000_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("islands");
assert_eq!(body.len(), 64);
let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
let third = u32::from_le_bytes(body[8..12].try_into().unwrap());
let island0 = branch_target(first, dst_base).unwrap();
let island1 = branch_target(third, dst_base + 8).unwrap();
assert_eq!(island0, dst_base + 32);
assert_eq!(island1, dst_base + 48);
assert_eq!(
u64::from_le_bytes(body[40..48].try_into().unwrap()),
branch_target(b0, src_base).unwrap()
);
assert_eq!(
u64::from_le_bytes(body[56..64].try_into().unwrap()),
branch_target(bl1, src_base + 8).unwrap()
);
}
#[test]
fn assemble_trampoline_refuses_unrelocatable_narrow_branch() {
let cond = bcond(0x2, 0x10);
let mut prologue = Vec::new();
for insn in [NOP, cond, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x0u64;
let dst_base = 0x0000_0080_0000u64;
let resume = src_base + STOLEN_BYTES as u64;
assert_eq!(
assemble_trampoline(&prologue, src_base, dst_base, resume),
None
);
}
#[test]
fn relocated_prologue_matches_assemble_prefix() {
let stp = 0xA9BF_7BFDu32;
let mut prologue = Vec::new();
for insn in [stp, NOP, NOP, NOP] {
prologue.extend_from_slice(&insn.to_le_bytes());
}
let src_base = 0x140_0010_0000u64;
let dst_base = 0x140_0030_0000u64;
let pro = relocated_prologue(&prologue, src_base, dst_base).expect("ok");
assert_eq!(pro.len(), 16);
assert_eq!(u32::from_le_bytes(pro[0..4].try_into().unwrap()), stp);
}
#[test]
fn island_holds_absolute_branch_to_original_target() {
let lead_bl = bl(0x100);
let src_base = 0x140_0010_0000u64;
let original_target = branch_target(lead_bl, src_base).unwrap();
let island = island_for_branch(lead_bl, src_base).expect("island");
assert_eq!(island.len(), 16);
assert_eq!(
u32::from_le_bytes(island[4..8].try_into().unwrap()),
BLR_X16
);
assert_eq!(
u64::from_le_bytes(island[8..16].try_into().unwrap()),
original_target
);
}
#[test]
fn stolen_bytes_is_four_instructions() {
assert_eq!(STOLEN_BYTES, 16);
assert_eq!(STOLEN_BYTES % 4, 0);
}
#[test]
fn encode_imm26_rejects_unaligned_and_overflow() {
assert_eq!(encode_imm26(3), None);
assert_eq!(encode_imm26(4), Some(1));
assert_eq!(encode_imm26(-4), Some((-1i32 as u32) & 0x03FF_FFFF));
assert_eq!(encode_imm26(1 << 27), None);
}
+1 -31
View File
@@ -2,9 +2,7 @@
import {EventEmitter} from 'node:events';
export type GameCaptureInjectionMethod = 'auto' | 'remote-thread' | 'set-windows-hook';
export type CaptureStrategyName = 'game-hook' | 'wgc' | 'dxgi-duplication' | 'window-gdi';
export type CaptureStrategyName = 'wgc' | 'dxgi-duplication' | 'window-gdi';
export interface ScreenCaptureRect {
x: number;
@@ -19,9 +17,6 @@ export interface ScreenCaptureOptions {
width?: number;
height?: number;
frameRate?: number;
hookDllPath?: string;
hookDllPathX86?: string;
injectionMethod?: GameCaptureInjectionMethod;
captureId?: string;
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
@@ -66,8 +61,6 @@ export interface CaptureDiagnostics {
droppedFrameCounter: number;
lastPresentTimestampUs: number;
lastError: number;
requestedInjectionMethod: GameCaptureInjectionMethod;
injectionMethod: 'remote-thread' | 'set-windows-hook';
activeStrategy: CaptureStrategyName;
lastFallbackReason: string;
startOptions: ScreenCaptureStartOptionsDiagnostics;
@@ -86,14 +79,6 @@ export interface ScreenCaptureStartOptionsDiagnostics {
unsupportedOptions: Array<'showCursorClicks' | 'captureRect' | 'colorRange' | 'colorSpace'>;
}
export interface SharedTextureHandleInfo {
handle: bigint;
width: number;
height: number;
dxgiFormat: number;
timestampUs: number;
}
export interface EncoderAttachDiagnostics {
attached: boolean;
width: number;
@@ -113,13 +98,6 @@ export interface FrameSinkDiagnostics {
cpuFallbackFramesDropped: number;
}
export interface VulkanLayerRegistrationState {
registered: boolean;
manifestExists: boolean;
dllExists: boolean;
manifestPath: string | null;
}
export declare interface ScreenCapture {
on(event: 'error', listener: (err: Error) => void): this;
on(event: 'closed', listener: () => void): this;
@@ -144,7 +122,6 @@ export declare class ScreenCapture extends EventEmitter {
start(): Promise<ScreenCaptureStartResult | undefined>;
stop(): Promise<void>;
getDiagnostics(): CaptureDiagnostics | null;
getSharedTextureHandle(): SharedTextureHandleInfo | null;
attachEncoder(width: number, height: number, frameRate?: number): void;
detachEncoder(): void;
isEncoderAttached(): boolean;
@@ -156,13 +133,6 @@ export declare class ScreenCapture extends EventEmitter {
export declare function isSupported(): boolean;
export declare function getAvailability(): AvailabilityInfo;
export declare function listSources(): Promise<Array<ScreenCaptureSourceDescriptor>>;
export declare function resolveGameHookPath(): string | null;
export declare function resolveGameHookPathX86(): string | null;
export declare function isGameCaptureHookAvailable(): boolean;
export declare function resolveVulkanLayerManifestPath(): string | null;
export declare function registerVulkanLayerManifest(): boolean;
export declare function unregisterVulkanLayerManifest(): boolean;
export declare function getVulkanLayerRegistrationState(): VulkanLayerRegistrationState;
export declare function parseFallbackRecommendation(message: string | undefined): CaptureStrategyName | 'none' | null;
export declare function elevateGpuSchedulingPriority(processId?: number, priorityClass?: 'high' | 'realtime'): boolean;
export declare function restoreGpuSchedulingPriority(processId?: number): boolean;
+1 -115
View File
@@ -59,97 +59,6 @@ if (process.platform === 'win32') {
});
}
function gameHookFileName(arch) {
switch (arch) {
case 'x64':
return 'fluxer-game-hook.win32-x64-msvc.dll';
case 'ia32':
return 'fluxer-game-hook.win32-ia32-msvc.dll';
case 'arm64':
return 'fluxer-game-hook.win32-arm64-msvc.dll';
default:
return null;
}
}
function resolveGameHookPathForArch(arch, root = nativeRoot) {
if (process.platform !== 'win32') return null;
const fileName = gameHookFileName(arch);
if (!fileName) return null;
const hookPath = join(root, fileName);
return existsSync(hookPath) ? hookPath : null;
}
function resolveGameHookPath(root = nativeRoot) {
return resolveGameHookPathForArch(process.arch, root);
}
function resolveGameHookPathX86(root = nativeRoot) {
return resolveGameHookPathForArch('ia32', root);
}
function vulkanLayerManifestFileName(arch) {
switch (arch) {
case 'x64':
return 'fluxer-vulkan-layer.win32-x64-msvc.json';
case 'ia32':
return 'fluxer-vulkan-layer.win32-ia32-msvc.json';
case 'arm64':
return 'fluxer-vulkan-layer.win32-arm64-msvc.json';
default:
return null;
}
}
function resolveVulkanLayerManifestPath(root = nativeRoot) {
if (process.platform !== 'win32') return null;
const fileName = vulkanLayerManifestFileName(process.arch);
if (!fileName) return null;
const manifestPath = join(root, fileName);
return existsSync(manifestPath) ? manifestPath : null;
}
function isGameCaptureHookAvailable(root = nativeRoot) {
if (typeof binding?.isGameCaptureHookAvailable !== 'function') return false;
if (binding.isGameCaptureHookAvailable() !== true) return false;
return resolveGameHookPath(root) !== null;
}
function registerVulkanLayerManifest(root = nativeRoot) {
if (!binding?.registerVulkanLayerManifest) return false;
if (!isGameCaptureHookAvailable(root)) return false;
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!manifestPath) return false;
binding.registerVulkanLayerManifest(manifestPath);
return true;
}
function unregisterVulkanLayerManifest(root = nativeRoot) {
if (!binding?.unregisterVulkanLayerManifest) return false;
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!manifestPath) return false;
try {
binding.unregisterVulkanLayerManifest(manifestPath);
return true;
} catch (error) {
console.warn('[win-game-capture] unregisterVulkanLayerManifest failed:', error?.message || error);
return false;
}
}
function getVulkanLayerRegistrationState(root = nativeRoot) {
const manifestPath = resolveVulkanLayerManifestPath(root);
if (!binding?.getVulkanLayerRegistrationState) {
return {registered: false, manifestExists: Boolean(manifestPath), dllExists: false, manifestPath};
}
try {
return binding.getVulkanLayerRegistrationState(manifestPath ?? '');
} catch (error) {
console.warn('[win-game-capture] getVulkanLayerRegistrationState failed:', error?.message || error);
return {registered: false, manifestExists: Boolean(manifestPath), dllExists: false, manifestPath};
}
}
class ScreenCapture extends EventEmitter {
constructor(options = {}) {
super();
@@ -161,9 +70,6 @@ class ScreenCapture extends EventEmitter {
this.width = options.width ?? 0;
this.height = options.height ?? 0;
this.frameRate = options.frameRate ?? 30;
this.hookDllPath = options.hookDllPath ?? resolveGameHookPath();
this.hookDllPathX86 = options.hookDllPathX86 ?? resolveGameHookPathX86();
this.injectionMethod = options.injectionMethod ?? undefined;
this.captureId = typeof options.captureId === 'string' ? options.captureId : undefined;
this.colorRange = options.colorRange;
this.colorSpace = options.colorSpace;
@@ -226,9 +132,6 @@ class ScreenCapture extends EventEmitter {
this.width || undefined,
this.height || undefined,
this.frameRate || undefined,
this.sourceKind === 'game' ? this.hookDllPath : undefined,
this.sourceKind === 'game' ? (this.hookDllPathX86 ?? undefined) : undefined,
this.sourceKind === 'game' ? (this.injectionMethod ?? undefined) : undefined,
this.captureId,
{
colorRange: this.colorRange,
@@ -272,16 +175,6 @@ class ScreenCapture extends EventEmitter {
}
}
getSharedTextureHandle() {
if (!this.native || typeof this.native.getSharedTextureHandle !== 'function') return null;
try {
return this.native.getSharedTextureHandle() ?? null;
} catch (error) {
console.warn('[win-game-capture] getSharedTextureHandle failed:', error?.message || error);
return null;
}
}
attachEncoder(width, height) {
if (!this.native || typeof this.native.attachEncoder !== 'function') {
throw new Error(`${MODULE_NAME} native binding does not support encoder attachment`);
@@ -346,7 +239,7 @@ class ScreenCapture extends EventEmitter {
}
}
const FALLBACK_STRATEGY_NAMES = new Set(['game-hook', 'wgc', 'dxgi-duplication', 'window-gdi', 'none']);
const FALLBACK_STRATEGY_NAMES = new Set(['wgc', 'dxgi-duplication', 'window-gdi', 'none']);
function parseFallbackRecommendation(message) {
if (typeof message !== 'string') return null;
const match = message.match(/\[next-strategy=([a-z-]+)\]/);
@@ -439,13 +332,6 @@ function __setBindingForTests(nextBinding) {
module.exports = {
isSupported,
getAvailability,
resolveGameHookPath,
resolveGameHookPathX86,
isGameCaptureHookAvailable,
resolveVulkanLayerManifestPath,
registerVulkanLayerManifest,
unregisterVulkanLayerManifest,
getVulkanLayerRegistrationState,
listSources,
ScreenCapture,
parseFallbackRecommendation,
@@ -22,8 +22,6 @@ function makeFakeBinding() {
const calls = [];
const frameSinkHandleCalls = [];
const priorityCalls = [];
const vulkanCalls = [];
const hookAvailable = {value: false};
const natives = [];
const diagnostics = {
state: 1,
@@ -38,9 +36,7 @@ function makeFakeBinding() {
droppedFrameCounter: 0,
lastPresentTimestampUs: 123456,
lastError: 0,
requestedInjectionMethod: 'auto',
injectionMethod: 'remote-thread',
activeStrategy: 'game-hook',
activeStrategy: 'wgc',
lastFallbackReason: '',
startOptions: {
colorRange: 'full',
@@ -88,27 +84,13 @@ function makeFakeBinding() {
frameSinkHandleCalls.push(handle);
}
start(
sourceId,
sourceKind,
width,
height,
frameRate,
hookDllPath,
hookDllPathX86,
injectionMethod,
captureId,
captureOptions,
) {
start(sourceId, sourceKind, width, height, frameRate, captureId, captureOptions) {
calls.push({
sourceId,
sourceKind,
width,
height,
frameRate,
hookDllPath,
hookDllPathX86,
injectionMethod,
captureId,
captureOptions,
});
@@ -190,19 +172,10 @@ function makeFakeBinding() {
restoreGpuSchedulingPriority: (processId) => {
priorityCalls.push({type: 'restore', processId});
},
isGameCaptureHookAvailable: () => hookAvailable.value,
registerVulkanLayerManifest: (manifestPath) => {
vulkanCalls.push({type: 'register', manifestPath});
},
unregisterVulkanLayerManifest: (manifestPath) => {
vulkanCalls.push({type: 'unregister', manifestPath});
},
},
calls,
frameSinkHandleCalls,
priorityCalls,
vulkanCalls,
hookAvailable,
natives,
diagnostics,
encoderDiagnostics,
@@ -258,74 +231,7 @@ describe('win-game-capture loader wrapper -- binding-absent fallback path', () =
);
});
describe('win-game-capture loader wrapper -- arch path resolvers (platform-portable)', () => {
test('resolveGameHookPath() is null or the host-arch hook path', () => {
const r = winGameCapture.resolveGameHookPath();
assert.ok(
r === null || (typeof r === 'string' && /fluxer-game-hook\.win32-(x64|ia32|arm64)-msvc\.dll$/.test(r)),
`unexpected resolveGameHookPath(): ${r}`,
);
});
test('resolveGameHookPathX86() is null or the ia32 hook path', () => {
const r = winGameCapture.resolveGameHookPathX86();
assert.ok(
r === null || (typeof r === 'string' && r.endsWith('fluxer-game-hook.win32-ia32-msvc.dll')),
`unexpected resolveGameHookPathX86(): ${r}`,
);
});
test('resolveVulkanLayerManifestPath() is null or the host-arch layer manifest path', () => {
const r = winGameCapture.resolveVulkanLayerManifestPath();
assert.ok(
r === null || (typeof r === 'string' && /fluxer-vulkan-layer\.win32-(x64|ia32|arm64)-msvc\.json$/.test(r)),
`unexpected resolveVulkanLayerManifestPath(): ${r}`,
);
});
});
describe('win-game-capture loader wrapper -- injected fake binding', () => {
test('isGameCaptureHookAvailable() follows the native hook flag', {skip: injectionSkip}, () => {
const {binding} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.isGameCaptureHookAvailable(), false);
});
test(
'isGameCaptureHookAvailable() is false when the native binding predates the hook flag',
{skip: injectionSkip},
() => {
const {binding} = makeFakeBinding();
binding.isGameCaptureHookAvailable = undefined;
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.isGameCaptureHookAvailable(), false);
},
);
test(
'registerVulkanLayerManifest() never touches the registry while hook capture is unavailable',
{skip: injectionSkip},
() => {
const {binding, vulkanCalls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.registerVulkanLayerManifest(), false);
assert.deepEqual(vulkanCalls, [], 'the native registration entry point must not be called');
},
);
test(
'registerVulkanLayerManifest() still refuses when the hook DLL is missing for this host',
{skip: injectionSkip || (winGameCapture.resolveGameHookPath() !== null && 'host ships a game capture hook DLL')},
() => {
const {binding, hookAvailable, vulkanCalls} = makeFakeBinding();
hookAvailable.value = true;
winGameCapture.__setBindingForTests(binding);
assert.equal(winGameCapture.isGameCaptureHookAvailable(), false);
assert.equal(winGameCapture.registerVulkanLayerManifest(), false);
assert.deepEqual(vulkanCalls, []);
},
);
test(
'listSources() forwards sanitized screen/window sources from the native binding',
{skip: injectionSkip},
@@ -355,7 +261,7 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
);
test(
'start() forwards sourceId/kind/dims/frameRate and BOTH hook paths (6th + 7th args)',
'start() forwards sourceId/kind/dims/frameRate/captureId and the capture options',
{skip: injectionSkip},
async () => {
const {binding, calls} = makeFakeBinding();
@@ -366,8 +272,6 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
width: 1600,
height: 900,
frameRate: 60,
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
captureId: 'capture-1',
colorRange: 'full',
colorSpace: 'rec709',
@@ -383,9 +287,6 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
width: 1600,
height: 900,
frameRate: 60,
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
injectionMethod: undefined,
captureId: 'capture-1',
captureOptions: {
colorRange: 'full',
@@ -401,40 +302,7 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
},
);
test(
'start() forwards the injectionMethod option as the 8th arg for game capture',
{skip: injectionSkip},
async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '555',
sourceKind: 'game',
hookDllPath: 'C:/hooks/fluxer-game-hook.win32-x64-msvc.dll',
hookDllPathX86: 'C:/hooks/fluxer-game-hook.win32-ia32-msvc.dll',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls.length, 1);
assert.equal(calls[0].injectionMethod, 'set-windows-hook');
},
);
test('window sourceKind does not forward the injectionMethod', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: '42',
sourceKind: 'window',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls[0].injectionMethod, undefined);
});
test('window sourceKind does not forward hook paths', {skip: injectionSkip}, async () => {
test('native start() takes no hook or injection arguments', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
@@ -445,27 +313,17 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls.length, 1);
assert.deepEqual(Object.keys(calls[0]), [
'sourceId',
'sourceKind',
'width',
'height',
'frameRate',
'captureId',
'captureOptions',
]);
assert.equal(calls[0].sourceKind, 'window');
assert.equal(calls[0].hookDllPath, undefined);
assert.equal(calls[0].hookDllPathX86, undefined);
});
test('screen sourceKind does not forward hook paths or the injectionMethod', {skip: injectionSkip}, async () => {
const {binding, calls} = makeFakeBinding();
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({
sourceId: 'screen:0:0',
sourceKind: 'screen',
hookDllPath: 'C:/hooks/primary.dll',
hookDllPathX86: 'C:/hooks/x86.dll',
injectionMethod: 'set-windows-hook',
});
capture.on('error', () => {});
await capture.start();
assert.equal(calls[0].sourceKind, 'screen');
assert.equal(calls[0].hookDllPath, undefined);
assert.equal(calls[0].hookDllPathX86, undefined);
assert.equal(calls[0].injectionMethod, undefined);
});
test('getDiagnostics() exposes native start option state', {skip: injectionSkip}, () => {
@@ -580,8 +438,6 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
const capture = new winGameCapture.ScreenCapture({
sourceId: '7',
sourceKind: 'game',
hookDllPath: '',
hookDllPathX86: '',
});
capture.on('error', () => {});
await capture.start();
@@ -739,8 +595,7 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
test('getDiagnostics() surfaces the activeStrategy + lastFallbackReason fields', {skip: injectionSkip}, () => {
const {binding, diagnostics} = makeFakeBinding();
diagnostics.activeStrategy = 'dxgi-duplication';
diagnostics.lastFallbackReason =
'game-hook capture could not inject its capture hook; switching to dxgi-duplication capture';
diagnostics.lastFallbackReason = 'wgc capture stopped delivering frames; switching to dxgi-duplication capture';
winGameCapture.__setBindingForTests(binding);
const capture = new winGameCapture.ScreenCapture({sourceId: '1'});
const snapshot = capture.getDiagnostics();
@@ -788,7 +643,7 @@ describe('win-game-capture loader wrapper -- injected fake binding', () => {
capture.on('error', (err) => errors.push(err));
natives[0].lifecycleCallback([
'error',
'fallback: game-hook -> dxgi-duplication (game-hook capture could not inject its capture hook) [next-strategy=dxgi-duplication]',
'fallback: wgc -> dxgi-duplication (wgc capture stopped delivering frames) [next-strategy=dxgi-duplication]',
]);
assert.equal(errors.length, 1);
assert.ok(errors[0] instanceof Error);
@@ -824,7 +679,7 @@ describe('win-game-capture loader wrapper -- parseFallbackRecommendation', () =>
test('extracts the recommended strategy from a transition error message', () => {
assert.equal(
winGameCapture.parseFallbackRecommendation(
'fallback: game-hook -> window-gdi (reason) [next-strategy=window-gdi]',
'fallback: dxgi-duplication -> window-gdi (reason) [next-strategy=window-gdi]',
),
'window-gdi',
);
@@ -853,7 +708,7 @@ describe('win-game-capture loader wrapper -- parseFallbackRecommendation', () =>
assert.equal(winGameCapture.parseFallbackRecommendation('[next-strategy=wgc]'), 'wgc');
assert.equal(
winGameCapture.parseFallbackRecommendation(
'fallback: game-hook -> wgc (game-hook capture could not inject its capture hook) [next-strategy=wgc]',
'fallback: dxgi-duplication -> wgc (dxgi-duplication capture stopped delivering frames) [next-strategy=wgc]',
),
'wgc',
);
@@ -1,25 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "fluxer_inject_helper"
version = "0.0.0"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
@@ -1,32 +0,0 @@
[package]
name = "fluxer_inject_helper"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[[bin]]
name = "fluxer-inject-helper"
path = "src/main.rs"
[profile.release]
# Keep the helper tiny: it is a one-shot injector exe shipped alongside the
# hook DLLs, so optimise hard for size and strip symbols.
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Threading",
]}
@@ -1,242 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![deny(clippy::all)]
#[cfg(not(target_os = "windows"))]
fn main() {
eprintln!("fluxer-inject-helper is only supported on Windows");
std::process::exit(Stage::Unsupported as i32);
}
#[repr(i32)]
#[derive(Clone, Copy)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
enum Stage {
Success = 0,
BadArgs = 2,
HookMissing = 3,
OpenProcess = 4,
Alloc = 5,
Write = 6,
Kernel32 = 7,
LoadLibraryAddr = 8,
CreateThread = 9,
WaitTimeout = 10,
LoadLibraryFailed = 11,
#[cfg_attr(target_os = "windows", allow(dead_code))]
Unsupported = 64,
}
#[cfg(target_os = "windows")]
fn main() {
let code = win::run();
std::process::exit(code as i32);
}
#[cfg(target_os = "windows")]
mod win {
use super::Stage;
use core::ffi::c_void;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, HANDLE, INVALID_HANDLE_VALUE, WAIT_ABANDONED, WAIT_OBJECT_0,
};
use windows_sys::Win32::System::Diagnostics::Debug::{OutputDebugStringW, WriteProcessMemory};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows_sys::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
};
use windows_sys::Win32::System::Threading::{
CreateRemoteThread, GetExitCodeThread, INFINITE, OpenProcess, PROCESS_CREATE_THREAD,
PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
WaitForSingleObject,
};
const DEFAULT_TIMEOUT_MS: u32 = 10_000;
struct OwnedHandle(HANDLE);
impl OwnedHandle {
fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
unsafe {
CloseHandle(self.0);
}
}
}
}
fn to_wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn log(message: &str) {
let text = format!("[fluxer-inject-helper] {message}");
let wide = to_wide(&text);
unsafe {
OutputDebugStringW(wide.as_ptr());
}
eprintln!("{text}");
}
fn fail(stage: Stage, context: &str) -> Stage {
let err = unsafe { GetLastError() };
log(&format!(
"FAILED stage={} ({context}); GetLastError={err}",
stage as i32
));
stage
}
pub(super) fn run() -> Stage {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() < 2 || args.len() > 3 {
log(&format!(
"bad args: expected <pid> <hook-dll-path> [timeout-ms], got {} arg(s)",
args.len()
));
return Stage::BadArgs;
}
let Ok(target_pid) = args[0].parse::<u32>() else {
log(&format!("bad args: unparseable pid {:?}", args[0]));
return Stage::BadArgs;
};
if target_pid == 0 {
log("bad args: pid must be non-zero");
return Stage::BadArgs;
}
let hook_path = args[1].as_str();
let timeout_ms = match args.get(2) {
None => DEFAULT_TIMEOUT_MS,
Some(raw) => match raw.parse::<u32>() {
Ok(0) => INFINITE,
Ok(value) => value,
Err(_) => {
log(&format!("bad args: unparseable timeout {raw:?}"));
return Stage::BadArgs;
}
},
};
if !std::path::Path::new(hook_path).exists() {
log(&format!("hook DLL missing: {hook_path}"));
return Stage::HookMissing;
}
log(&format!(
"injecting (pid={target_pid}, hook={hook_path}, timeout_ms={timeout_ms}, \
helper_bits={})",
usize::BITS
));
inject(target_pid, hook_path, timeout_ms)
}
fn inject(target_pid: u32, hook_path: &str, timeout_ms: u32) -> Stage {
let wide_path = to_wide(hook_path);
let path_bytes = wide_path.len() * std::mem::size_of::<u16>();
unsafe {
let process = OpenProcess(
PROCESS_CREATE_THREAD
| PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ
| PROCESS_QUERY_INFORMATION,
0,
target_pid,
);
if process.is_null() {
return fail(Stage::OpenProcess, "OpenProcess returned null");
}
let process = OwnedHandle(process);
let remote_path = VirtualAllocEx(
process.raw(),
null(),
path_bytes,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
);
if remote_path.is_null() {
return fail(Stage::Alloc, "VirtualAllocEx returned null");
}
let mut written: usize = 0;
let write_ok = WriteProcessMemory(
process.raw(),
remote_path,
wide_path.as_ptr().cast(),
path_bytes,
&mut written,
) != 0;
if !write_ok || written != path_bytes {
let stage = fail(Stage::Write, "WriteProcessMemory failed/short");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let kernel32_name = to_wide("kernel32.dll");
let kernel32 = GetModuleHandleW(kernel32_name.as_ptr());
if kernel32.is_null() {
let stage = fail(Stage::Kernel32, "GetModuleHandleW(kernel32.dll)");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr().cast());
let Some(load_library) = load_library else {
let stage = fail(Stage::LoadLibraryAddr, "GetProcAddress(LoadLibraryW)");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
};
let start_routine: unsafe extern "system" fn(*mut c_void) -> u32 =
std::mem::transmute(load_library);
let thread = CreateRemoteThread(
process.raw(),
null(),
0,
Some(start_routine),
remote_path,
0,
null_mut(),
);
if thread.is_null() {
let stage = fail(Stage::CreateThread, "CreateRemoteThread returned null");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let thread = OwnedHandle(thread);
let wait = WaitForSingleObject(thread.raw(), timeout_ms);
if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED {
let stage = fail(Stage::WaitTimeout, "WaitForSingleObject did not signal");
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
return stage;
}
let mut exit_code: u32 = 0;
let got_exit = GetExitCodeThread(thread.raw(), &mut exit_code) != 0;
VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE);
if !got_exit {
return fail(Stage::LoadLibraryFailed, "GetExitCodeThread failed");
}
if exit_code == 0 {
log("remote LoadLibraryW returned NULL -- DLL failed to load in target");
return Stage::LoadLibraryFailed;
}
log(&format!(
"injection succeeded (remote LoadLibraryW HMODULE low bits={exit_code:#010x})"
));
Stage::Success
}
}
}
@@ -1,610 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#[cfg(target_os = "windows")]
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InjectionPolicy {
Allow,
ForceCpuReadback,
Deny(String),
}
const HARD_DENY_PROCESS_NAMES: &[(&str, &str)] = &[
("easyanticheat.exe", "EasyAntiCheat"),
("easyanticheat_eos.exe", "EasyAntiCheat"),
("easyanticheat_launcher.exe", "EasyAntiCheat"),
("eac.exe", "EasyAntiCheat"),
("eac_launcher.exe", "EasyAntiCheat"),
("beservice.exe", "BattlEye"),
("beservice_x64.exe", "BattlEye"),
("bedaisy.exe", "BattlEye"),
("be_service.exe", "BattlEye"),
("vgc.exe", "Riot Vanguard"),
("vgk.exe", "Riot Vanguard"),
("vgtray.exe", "Riot Vanguard"),
("vanguard.exe", "Riot Vanguard"),
("destiny2.exe", "Destiny 2 anti-cheat policy"),
("equ8.exe", "EQU8 anti-cheat"),
("equ8_service.exe", "EQU8 anti-cheat"),
("gameguard.des", "nProtect GameGuard"),
("gamemon.des", "nProtect GameGuard"),
("gamemon64.des", "nProtect GameGuard"),
("npggnt.des", "nProtect GameGuard"),
("xigncode.exe", "XIGNCODE"),
("xigncode3.exe", "XIGNCODE3"),
("mhyprot.exe", "mhyprot anti-cheat"),
("mhyprot2.exe", "mhyprot anti-cheat"),
("anticheatexpert.exe", "Anti-Cheat Expert"),
("ace-base.exe", "Anti-Cheat Expert"),
("faceit.exe", "FACEIT Anti-cheat"),
("faceitclient.exe", "FACEIT Anti-cheat"),
("faceitservice.exe", "FACEIT Anti-cheat"),
("esea.exe", "ESEA Anti-cheat"),
("eseaclient.exe", "ESEA Anti-cheat"),
("eseaservice.exe", "ESEA Anti-cheat"),
("punkbuster.exe", "PunkBuster"),
("pnkbstra.exe", "PunkBuster"),
("pnkbstrb.exe", "PunkBuster"),
("system", "Windows kernel process"),
("csrss.exe", "Windows system process"),
("smss.exe", "Windows system process"),
("wininit.exe", "Windows system process"),
("winlogon.exe", "Windows system process"),
("services.exe", "Windows system process"),
("svchost.exe", "Windows service host"),
("dwm.exe", "Windows compositor"),
("fontdrvhost.exe", "Windows font driver host"),
("logonui.exe", "Windows secure desktop"),
("consent.exe", "Windows secure desktop"),
("secureuxhost.exe", "Windows secure desktop"),
("lsass.exe", "Windows security process"),
("lsaiso.exe", "Windows security process"),
("msmpeng.exe", "Microsoft Defender"),
("securityhealthservice.exe", "Windows Security"),
("securityhealthsystray.exe", "Windows Security"),
("audiodg.exe", "Windows protected audio graph"),
("wudfhost.exe", "Windows driver host"),
("taskhostw.exe", "Windows task host"),
("dllhost.exe", "Windows COM surrogate"),
("runtimebroker.exe", "Windows runtime broker"),
("applicationframehost.exe", "Windows application frame host"),
("lockapp.exe", "Windows lock screen"),
("sihost.exe", "Windows shell infrastructure"),
("startmenuexperiencehost.exe", "Windows shell"),
("searchhost.exe", "Windows shell"),
("searchapp.exe", "Windows shell"),
("textinputhost.exe", "Windows shell"),
("explorer.exe", "Windows shell"),
("taskmgr.exe", "Windows administrative tool"),
("regedit.exe", "Windows administrative tool"),
("mmc.exe", "Windows administrative tool"),
("obs32.exe", "capture application"),
("obs64.exe", "capture application"),
("fluxer.exe", "Fluxer application"),
("fluxer-desktop.exe", "Fluxer application"),
("fluxer_desktop.exe", "Fluxer application"),
];
const COMPATIBILITY_DENY_PROCESS_NAMES: &[(&str, &str)] = &[
("gta-sa.exe", "legacy D3D8/RenderWare compatibility"),
("samp.exe", "legacy D3D8/RenderWare compatibility"),
("leagueclientux.exe", "League of Legends launcher"),
("steamwebhelper.exe", "Chromium-based launcher"),
("epicgameslauncher.exe", "Chromium-based launcher"),
("riotclientux.exe", "Riot client"),
("riotclientservices.exe", "Riot client"),
("battle.net.exe", "Chromium-based launcher"),
("gamingservices.exe", "Xbox Gaming Services"),
("gamingservicesnet.exe", "Xbox Gaming Services"),
];
const COMPATIBILITY_DENY_WINDOW_CLASSES: &[(&str, &str)] = &[
("chrome_widgetwin_0", "Chromium-based game window"),
("chrome_widgetwin_1", "Chromium-based game window"),
(
"gamingservicesui_hosting_window_class",
"Xbox Gaming Services",
),
];
const FORCE_CPU_PROCESS_NAMES: &[&str] = &["terraria.exe"];
const OVERRIDE_FILE_NAME: &str = "compatibility.json";
pub fn injection_policy(target_pid: u32) -> InjectionPolicy {
let exe_name = match target_process_exe_name(target_pid) {
Some(name) => name,
None => {
return InjectionPolicy::Allow;
}
};
evaluate_policy(&exe_name, None, load_override())
}
#[cfg(target_os = "windows")]
pub fn injection_policy_for_window(
target_pid: u32,
hwnd: windows_sys::Win32::Foundation::HWND,
) -> InjectionPolicy {
let exe_name = match target_process_exe_name(target_pid) {
Some(name) => name,
None => {
return InjectionPolicy::Allow;
}
};
evaluate_policy(
&exe_name,
target_window_class_name(hwnd).as_deref(),
load_override(),
)
}
fn evaluate_policy(
exe_name: &str,
window_class: Option<&str>,
override_lists: Option<OverrideLists>,
) -> InjectionPolicy {
let exe_name_lower = file_name_lower(exe_name);
let exe_name_lower = exe_name_lower.as_str();
let window_class_lower = window_class.map(|name| name.trim().to_ascii_lowercase());
let window_class_lower = window_class_lower.as_deref();
if let Some(lists) = override_lists.as_ref() {
if let Some(reason) = embedded_hard_deny_reason(exe_name_lower) {
return reason;
}
if lists.deny.iter().any(|name| name == exe_name_lower) {
return InjectionPolicy::Deny(format!(
"{exe_name_lower} is on the local compatibility deny list; Fluxer will not inject \
its game-capture hook"
));
}
let allowed_by_override = lists.allow.iter().any(|name| name == exe_name_lower);
if !allowed_by_override
&& let Some(reason) = embedded_compatibility_deny_reason(exe_name_lower)
{
return reason;
}
if !allowed_by_override
&& let Some(reason) = embedded_window_class_deny_reason(window_class_lower)
{
return reason;
}
if lists.force_cpu.iter().any(|name| name == exe_name_lower) {
return InjectionPolicy::ForceCpuReadback;
}
if allowed_by_override {
return InjectionPolicy::Allow;
}
} else {
if let Some(reason) = embedded_hard_deny_reason(exe_name_lower) {
return reason;
}
if let Some(reason) = embedded_compatibility_deny_reason(exe_name_lower) {
return reason;
}
if let Some(reason) = embedded_window_class_deny_reason(window_class_lower) {
return reason;
}
}
if FORCE_CPU_PROCESS_NAMES.contains(&exe_name_lower) {
return InjectionPolicy::ForceCpuReadback;
}
InjectionPolicy::Allow
}
fn embedded_hard_deny_reason(exe_name_lower: &str) -> Option<InjectionPolicy> {
HARD_DENY_PROCESS_NAMES
.iter()
.find(|(name, _)| *name == exe_name_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"{exe_name_lower} is protected by {label}; Fluxer will not inject its game-capture \
hook into anti-cheat or security-sensitive processes"
))
})
}
fn embedded_compatibility_deny_reason(exe_name_lower: &str) -> Option<InjectionPolicy> {
COMPATIBILITY_DENY_PROCESS_NAMES
.iter()
.find(|(name, _)| *name == exe_name_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"{exe_name_lower} has known game-capture compatibility issues ({label}); Fluxer \
will not inject its game-capture hook by default"
))
})
}
fn embedded_window_class_deny_reason(window_class_lower: Option<&str>) -> Option<InjectionPolicy> {
let window_class_lower = window_class_lower?;
COMPATIBILITY_DENY_WINDOW_CLASSES
.iter()
.find(|(name, _)| *name == window_class_lower)
.map(|(_, label)| {
InjectionPolicy::Deny(format!(
"window class {window_class_lower} has known OBS game-capture compatibility issues \
({label}); Fluxer will not inject its game-capture hook by default"
))
})
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct OverrideLists {
deny: Vec<String>,
allow: Vec<String>,
force_cpu: Vec<String>,
}
impl OverrideLists {
fn is_empty(&self) -> bool {
self.deny.is_empty() && self.allow.is_empty() && self.force_cpu.is_empty()
}
}
fn load_override() -> Option<OverrideLists> {
let path = override_file_path()?;
let contents = std::fs::read_to_string(&path).ok()?;
let lists = parse_override_json(&contents);
if lists.is_empty() { None } else { Some(lists) }
}
fn override_file_path() -> Option<PathBuf> {
if let Some(dir) = addon_directory() {
let candidate = dir.join(OVERRIDE_FILE_NAME);
if candidate.is_file() {
return Some(candidate);
}
}
let exe_dir = std::env::current_exe().ok()?.parent()?.to_path_buf();
let candidate = exe_dir.join(OVERRIDE_FILE_NAME);
if candidate.is_file() {
Some(candidate)
} else {
None
}
}
#[cfg(target_os = "windows")]
fn addon_directory() -> Option<PathBuf> {
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::HMODULE;
use windows_sys::Win32::System::LibraryLoader::{
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
GetModuleFileNameW, GetModuleHandleExW,
};
let mut module: HMODULE = std::ptr::null_mut();
let ok = unsafe {
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
addon_directory as *const u16,
&mut module,
)
};
if ok == 0 || module.is_null() {
return None;
}
let mut buffer = vec![0u16; 1024];
let len = unsafe { GetModuleFileNameW(module, buffer.as_mut_ptr(), buffer.len() as u32) };
if len == 0 || len as usize >= buffer.len() {
return None;
}
buffer.truncate(len as usize);
let module_path = PathBuf::from(std::ffi::OsString::from_wide(&buffer));
module_path.parent().map(Path::to_path_buf)
}
#[cfg(not(target_os = "windows"))]
fn addon_directory() -> Option<PathBuf> {
None
}
#[cfg(target_os = "windows")]
fn target_process_exe_name(pid: u32) -> Option<String> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
};
if pid == 0 {
return None;
}
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return None;
}
let mut buffer = vec![0u16; 1024];
let mut size = buffer.len() as u32;
let ok = unsafe {
QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, buffer.as_mut_ptr(), &mut size)
};
unsafe {
CloseHandle(handle);
}
if ok == 0 || size == 0 || size as usize > buffer.len() {
return None;
}
let full_path: String = String::from_utf16_lossy(&buffer[..size as usize]);
Some(file_name_lower(&full_path))
}
#[cfg(target_os = "windows")]
fn target_window_class_name(hwnd: windows_sys::Win32::Foundation::HWND) -> Option<String> {
use windows_sys::Win32::UI::WindowsAndMessaging::GetClassNameW;
if hwnd.is_null() {
return None;
}
let mut buffer = vec![0u16; 256];
let len = unsafe { GetClassNameW(hwnd, buffer.as_mut_ptr(), buffer.len() as i32) };
if len <= 0 {
return None;
}
buffer.truncate(len as usize);
Some(
String::from_utf16_lossy(&buffer)
.trim()
.to_ascii_lowercase(),
)
}
#[cfg(not(target_os = "windows"))]
fn target_process_exe_name(_pid: u32) -> Option<String> {
None
}
fn file_name_lower(path: &str) -> String {
path.rsplit(['\\', '/'])
.next()
.unwrap_or(path)
.trim()
.to_ascii_lowercase()
}
fn parse_override_json(text: &str) -> OverrideLists {
let mut force_cpu = extract_string_array(text, "forceCpu");
force_cpu.extend(extract_string_array(text, "force_cpu"));
OverrideLists {
deny: extract_string_array(text, "deny"),
allow: extract_string_array(text, "allow"),
force_cpu,
}
}
fn extract_string_array(text: &str, key: &str) -> Vec<String> {
let needle = format!("\"{key}\"");
let mut search_from = 0usize;
while let Some(rel) = text[search_from..].find(&needle) {
let key_pos = search_from + rel;
let after_key = key_pos + needle.len();
let rest = text[after_key..].trim_start();
if let Some(rest) = rest.strip_prefix(':') {
let rest = rest.trim_start();
if let Some(array_body) = rest.strip_prefix('[')
&& let Some(end) = array_body.find(']')
{
return parse_json_string_list(&array_body[..end]);
}
}
search_from = after_key;
}
Vec::new()
}
fn parse_json_string_list(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = body.char_indices().peekable();
while let Some((_, ch)) = chars.next() {
if ch != '"' {
continue;
}
let mut value = String::new();
let mut closed = false;
while let Some((_, c)) = chars.next() {
match c {
'\\' => {
if let Some((_, escaped)) = chars.next() {
match escaped {
'n' => value.push('\n'),
't' => value.push('\t'),
'r' => value.push('\r'),
other => value.push(other),
}
}
}
'"' => {
closed = true;
break;
}
other => value.push(other),
}
}
if closed {
let normalised = file_name_lower(&value);
if !normalised.is_empty() {
out.push(normalised);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_deny_matches_anticheat() {
match evaluate_policy("easyanticheat.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("EasyAntiCheat")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("beservice.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("BattlEye")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("vgc.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Vanguard")),
other => panic!("expected Deny, got {other:?}"),
}
match evaluate_policy("destiny2.exe", None, None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Destiny 2")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn embedded_deny_matches_security_sensitive_windows_processes() {
for name in [
"lsass.exe",
"dwm.exe",
"explorer.exe",
"applicationframehost.exe",
"obs64.exe",
"fluxer-desktop.exe",
] {
match evaluate_policy(name, None, None) {
InjectionPolicy::Deny(reason) => {
assert!(reason.contains("security-sensitive") || reason.contains("protected"))
}
other => panic!("expected Deny for {name}, got {other:?}"),
}
}
}
#[test]
fn unknown_process_is_allowed() {
assert_eq!(
evaluate_policy("mygame.exe", None, None),
InjectionPolicy::Allow
);
}
#[test]
fn override_deny_wins() {
let lists = parse_override_json(r#"{ "deny": ["MyGame.exe"] }"#);
match evaluate_policy("mygame.exe", None, Some(lists)) {
InjectionPolicy::Deny(_) => {}
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn override_allow_cannot_unblock_hard_deny() {
let lists = parse_override_json(r#"{ "allow": ["easyanticheat.exe"] }"#);
match evaluate_policy("easyanticheat.exe", None, Some(lists)) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("EasyAntiCheat")),
other => panic!("expected hard Deny, got {other:?}"),
}
}
#[test]
fn override_allow_unblocks_soft_compatibility_deny() {
let lists = parse_override_json(r#"{ "allow": ["LeagueClientUx.exe"] }"#);
assert_eq!(
evaluate_policy("LeagueClientUx.exe", None, Some(lists)),
InjectionPolicy::Allow
);
}
#[test]
fn override_allow_plus_force_cpu_unblocks_soft_deny_with_cpu_readback() {
let lists = parse_override_json(
r#"{ "allow": ["LeagueClientUx.exe"], "forceCpu": ["LeagueClientUx.exe"] }"#,
);
assert_eq!(
evaluate_policy("LeagueClientUx.exe", None, Some(lists)),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn override_force_cpu_applies() {
let lists = parse_override_json(r#"{ "forceCpu": ["weird.exe"] }"#);
assert_eq!(
evaluate_policy("weird.exe", None, Some(lists)),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn embedded_force_cpu_applies_for_known_cross_adapter_case() {
assert_eq!(
evaluate_policy("Terraria.exe", None, None),
InjectionPolicy::ForceCpuReadback
);
}
#[test]
fn local_force_cpu_cannot_override_hard_deny() {
let lists = parse_override_json(r#"{ "forceCpu": ["lsass.exe"] }"#);
match evaluate_policy("lsass.exe", None, Some(lists)) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("security-sensitive")),
other => panic!("expected hard Deny, got {other:?}"),
}
}
#[test]
fn force_cpu_snake_case_alias_parses() {
let lists = parse_override_json(r#"{ "force_cpu": ["weird.exe"] }"#);
assert!(lists.force_cpu.contains(&"weird.exe".to_string()));
}
#[test]
fn embedded_window_class_deny_matches_obs_chromium_game_windows() {
match evaluate_policy("game.exe", Some("Chrome_WidgetWin_1"), None) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Chromium")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn embedded_window_class_deny_matches_xbox_gaming_services() {
match evaluate_policy(
"game.exe",
Some("GamingServicesUI_Hosting_Window_Class"),
None,
) {
InjectionPolicy::Deny(reason) => assert!(reason.contains("Xbox Gaming Services")),
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn override_allow_unblocks_soft_window_class_deny() {
let lists = parse_override_json(r#"{ "allow": ["game.exe"] }"#);
assert_eq!(
evaluate_policy("game.exe", Some("Chrome_WidgetWin_0"), Some(lists)),
InjectionPolicy::Allow
);
}
#[test]
fn parser_normalises_paths_and_ignores_garbage() {
let lists = parse_override_json(
r#"{ "deny": ["C:\\Games\\Foo\\Foo.exe", "/opt/bar/Bar.EXE", 123, null] }"#,
);
assert_eq!(lists.deny, vec!["foo.exe", "bar.exe"]);
}
#[test]
fn malformed_json_yields_empty() {
let lists = parse_override_json("not json at all");
assert!(lists.is_empty());
}
#[test]
fn file_name_lower_handles_both_separators() {
assert_eq!(file_name_lower("C:\\A\\B\\Game.EXE"), "game.exe");
assert_eq!(file_name_lower("/a/b/Game.EXE"), "game.exe");
assert_eq!(file_name_lower("bare.exe"), "bare.exe");
}
}
@@ -393,16 +393,6 @@ pub(crate) fn resolve_output_size(
)
}
#[cfg(target_os = "windows")]
#[cfg(feature = "game-capture-hook")]
pub(crate) fn wall_clock_us() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros() as i64)
.unwrap_or(0)
}
#[cfg(target_os = "windows")]
pub(crate) fn capture_timestamp_us(capture_start: std::time::Instant) -> i64 {
let elapsed_us = capture_start.elapsed().as_micros();
File diff suppressed because it is too large Load Diff
@@ -5,16 +5,12 @@
#[cfg(target_os = "windows")]
mod capture_target;
#[cfg(any(all(target_os = "windows", feature = "game-capture-hook"), test))]
mod compatibility;
#[cfg(target_os = "windows")]
mod d3d11_device;
#[cfg(any(target_os = "windows", test))]
mod dxgi_capture;
pub mod encoder_attach;
mod fallback;
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
mod game_capture;
mod game_capture_abi;
mod gpu_priority;
mod hdr;
@@ -23,8 +19,6 @@ mod nv12_gpu;
mod sources;
#[cfg(any(target_os = "windows", test))]
mod stall;
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
mod vulkan_layer_registry;
#[cfg(target_os = "windows")]
mod wgc_capture;
@@ -43,8 +37,6 @@ use dxgi_capture::DxgiCaptureSession;
use fluxer_encoder_ring::EncoderFrameRate;
#[cfg(target_os = "windows")]
use fluxer_screen_frame_bus::EnqueueOutcome;
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
use game_capture::GameCaptureSession;
#[cfg(target_os = "windows")]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
#[cfg(target_os = "windows")]
@@ -152,10 +144,6 @@ pub struct CaptureDiagnostics {
pub last_present_timestamp_us: f64,
#[napi(js_name = "lastError")]
pub last_error: u32,
#[napi(js_name = "requestedInjectionMethod")]
pub requested_injection_method: String,
#[napi(js_name = "injectionMethod")]
pub injection_method: String,
#[napi(js_name = "activeStrategy")]
pub active_strategy: String,
#[napi(js_name = "lastFallbackReason")]
@@ -201,36 +189,12 @@ pub struct FrameSinkDiagnostics {
pub cpu_fallback_frames_dropped: f64,
}
#[napi(object)]
pub struct SharedTextureHandleInfo {
pub handle: BigInt,
pub width: u32,
pub height: u32,
#[napi(js_name = "dxgiFormat")]
pub dxgi_format: u32,
#[napi(js_name = "timestampUs")]
pub timestamp_us: f64,
}
#[napi(object)]
pub struct VulkanLayerRegistrationState {
pub registered: bool,
#[napi(js_name = "manifestExists")]
pub manifest_exists: bool,
#[napi(js_name = "dllExists")]
pub dll_exists: bool,
#[napi(js_name = "manifestPath")]
pub manifest_path: String,
}
pub struct CaptureInner {
pub lifecycle_tsfn: Mutex<Option<LifecycleTsfn>>,
#[cfg(target_os = "windows")]
pub session: Mutex<Option<DxgiCaptureSession>>,
#[cfg(target_os = "windows")]
pub(crate) wgc_session: Mutex<Option<WgcCaptureSession>>,
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
pub game_session: Mutex<Option<Arc<GameCaptureSession>>>,
pub running: std::sync::atomic::AtomicBool,
pub fallback: Mutex<Option<fallback::FallbackTracker>>,
pub capture_id: Mutex<Option<String>>,
@@ -423,18 +387,6 @@ pub(crate) fn note_media_frame_without_sink(inner: &CaptureInner, message: &'sta
emit_lifecycle(inner, "diagnostic", message);
}
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
pub(crate) fn note_cpu_fallback_frame_dropped(inner: &CaptureInner, message: &'static str) {
inner
.cpu_fallback_frames_dropped
.fetch_add(1, Ordering::AcqRel);
if inner.cpu_fallback_emitted.swap(true, Ordering::AcqRel) {
return;
}
emit_lifecycle(inner, "diagnostic", message);
}
#[cfg(target_os = "windows")]
fn native_frame_sink_for(
inner: &CaptureInner,
@@ -474,8 +426,6 @@ impl ScreenCapture {
session: Mutex::new(None),
#[cfg(target_os = "windows")]
wgc_session: Mutex::new(None),
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
game_session: Mutex::new(None),
running: std::sync::atomic::AtomicBool::new(false),
fallback: Mutex::new(None),
capture_id: Mutex::new(None),
@@ -533,9 +483,6 @@ impl ScreenCapture {
width: Option<u32>,
height: Option<u32>,
frame_rate: Option<u32>,
hook_path: Option<String>,
hook_path_x86: Option<String>,
injection_method: Option<String>,
capture_id: Option<String>,
start_options: Option<ScreenCaptureStartOptions>,
) -> Result<CaptureStartResult> {
@@ -555,9 +502,6 @@ impl ScreenCapture {
width,
height,
frame_rate,
hook_path,
hook_path_x86,
injection_method,
start_options,
)
}
@@ -569,9 +513,6 @@ impl ScreenCapture {
width,
height,
frame_rate,
hook_path,
hook_path_x86,
injection_method,
start_options,
);
Err(napi::Error::from_reason(
@@ -590,53 +531,8 @@ impl ScreenCapture {
#[cfg(target_os = "windows")]
{
let frame_sink = frame_sink_counter_snapshot(&self.inner);
#[cfg(feature = "game-capture-hook")]
{
let guard = self.inner.game_session.lock();
if let Some(session) = guard.as_ref() {
let requested_injection_method =
session.requested_injection_method().to_string();
let injection_method = session.used_injection_method().to_string();
if let Some(info) = session.read_shared_info() {
return Some(CaptureDiagnostics {
state: info.state,
api_type: info.api_type,
transport: info.transport,
fallback_reason: info.fallback_reason,
capture_flags: info.capture_flags,
width: info.width,
height: info.height,
dxgi_format: info.dxgi_format,
frame_counter: info.frame_counter as f64,
dropped_frame_counter: info.dropped_frame_counter as f64,
last_present_timestamp_us: info.last_present_timestamp_us as f64,
last_error: info.last_error,
requested_injection_method,
injection_method,
active_strategy: snapshot.active_strategy,
last_fallback_reason: snapshot.last_fallback_reason,
start_options: current_start_options(&self.inner),
frame_sink_accepted: frame_sink.accepted as f64,
frame_sink_coalesced: frame_sink.coalesced as f64,
frame_sink_rejected: frame_sink.rejected as f64,
media_frames_dropped_without_sink: frame_sink.dropped_without_sink
as f64,
cpu_fallback_frames_dropped: frame_sink.cpu_fallback_dropped as f64,
});
}
return Some(strategy_only_diagnostics(
&snapshot,
requested_injection_method,
injection_method,
current_start_options(&self.inner),
frame_sink,
));
}
}
Some(strategy_only_diagnostics(
&snapshot,
String::new(),
String::new(),
current_start_options(&self.inner),
frame_sink,
))
@@ -645,8 +541,6 @@ impl ScreenCapture {
#[cfg(not(target_os = "windows"))]
Some(strategy_only_diagnostics(
&snapshot,
String::new(),
String::new(),
current_start_options(&self.inner),
FrameSinkCounterSnapshot {
accepted: 0,
@@ -676,29 +570,6 @@ impl ScreenCapture {
}
}
#[napi(js_name = "getSharedTextureHandle")]
pub fn get_shared_texture_handle(&self) -> Option<SharedTextureHandleInfo> {
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let guard = self.inner.game_session.lock();
let session = guard.as_ref()?;
if let Some(native_texture) = session.read_native_texture_info() {
return Some(SharedTextureHandleInfo {
handle: BigInt::from(native_texture.handle),
width: native_texture.width,
height: native_texture.height,
dxgi_format: native_texture.dxgi_format,
timestamp_us: native_texture.timestamp_us as f64,
});
}
None
}
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
None
}
}
#[napi]
pub fn stop(&self) -> Result<()> {
self.inner
@@ -716,11 +587,6 @@ impl ScreenCapture {
let mut wgc_guard = self.inner.wgc_session.lock();
*wgc_guard = None;
}
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let mut game_guard = self.inner.game_session.lock();
*game_guard = None;
}
{
let mut fallback_guard = self.inner.fallback.lock();
*fallback_guard = None;
@@ -956,9 +822,6 @@ impl ScreenCapture {
width: Option<u32>,
height: Option<u32>,
frame_rate: Option<u32>,
hook_path: Option<String>,
hook_path_x86: Option<String>,
injection_method: Option<String>,
_start_options: CaptureStartOptionsDiagnostics,
) -> Result<CaptureStartResult> {
use std::sync::atomic::Ordering;
@@ -967,21 +830,6 @@ impl ScreenCapture {
return Err(napi::Error::from_reason("Capture already running"));
}
#[cfg(feature = "game-capture-hook")]
if source_kind == "game" {
return self.start_windows_game(
source_id,
source_kind,
width,
height,
frame_rate,
hook_path,
hook_path_x86,
injection_method,
_start_options,
);
}
let _ = (hook_path, hook_path_x86, injection_method);
let target_frame_rate = frame_rate.unwrap_or(30).clamp(1, 144);
let frame_interval =
@@ -1127,86 +975,10 @@ impl ScreenCapture {
pixel_format: "bgra".to_string(),
})
}
#[cfg(feature = "game-capture-hook")]
#[allow(clippy::too_many_arguments)]
fn start_windows_game(
&self,
source_id: String,
source_kind: String,
width: Option<u32>,
height: Option<u32>,
frame_rate: Option<u32>,
hook_path: Option<String>,
hook_path_x86: Option<String>,
injection_method: Option<String>,
_start_options: CaptureStartOptionsDiagnostics,
) -> Result<CaptureStartResult> {
use std::sync::atomic::Ordering;
if game_capture_abi::env_flag_enabled(game_capture_abi::ENV_DISABLE_HOOK) {
return Err(napi::Error::from_reason(
"game capture hook disabled via FLUXER_GAME_CAPTURE_DISABLE_HOOK",
));
}
let hook_path = hook_path
.ok_or_else(|| napi::Error::from_reason("missing game capture hook DLL path"))?;
let target_frame_rate = frame_rate.unwrap_or(30).clamp(1, 144);
let session = GameCaptureSession::new(
&source_id,
&source_kind,
width,
height,
target_frame_rate,
&hook_path,
hook_path_x86.as_deref(),
injection_method.as_deref(),
)
.map_err(|e| napi::Error::from_reason(format!("Failed to create game capture: {e}")))?;
let capture_width = session.capture_width();
let capture_height = session.capture_height();
let session = Arc::new(session);
{
let mut guard = self.inner.game_session.lock();
*guard = Some(session);
}
{
let mut guard = self.inner.fallback.lock();
*guard = Some(fallback::FallbackTracker::new(
fallback::CaptureStrategy::GameHook,
));
}
self.inner.running.store(true, Ordering::Release);
let inner = Arc::clone(&self.inner);
let frame_interval =
std::time::Duration::from_nanos(1_000_000_000 / target_frame_rate as u64);
std::thread::Builder::new()
.name("game-capture".into())
.spawn(move || {
game_capture::capture_loop(&inner, frame_interval);
})
.map_err(|e| {
napi::Error::from_reason(format!("Failed to spawn game capture thread: {e}"))
})?;
Ok(CaptureStartResult {
width: capture_width,
height: capture_height,
frame_rate: target_frame_rate,
pixel_format: "bgra".to_string(),
})
}
}
fn strategy_only_diagnostics(
snapshot: &fallback::FallbackSnapshot,
requested_injection_method: String,
injection_method: String,
start_options: CaptureStartOptionsDiagnostics,
frame_sink: FrameSinkCounterSnapshot,
) -> CaptureDiagnostics {
@@ -1223,8 +995,6 @@ fn strategy_only_diagnostics(
dropped_frame_counter: 0.0,
last_present_timestamp_us: 0.0,
last_error: 0,
requested_injection_method,
injection_method,
active_strategy: snapshot.active_strategy.clone(),
last_fallback_reason: snapshot.last_fallback_reason.clone(),
start_options,
@@ -1290,11 +1060,6 @@ pub fn is_supported() -> bool {
cfg!(target_os = "windows")
}
#[napi(js_name = "isGameCaptureHookAvailable")]
pub fn is_game_capture_hook_available() -> bool {
cfg!(all(target_os = "windows", feature = "game-capture-hook"))
}
#[napi(js_name = "getAvailability")]
pub fn get_availability() -> AvailabilityInfo {
AvailabilityInfo {
@@ -1325,56 +1090,3 @@ pub fn elevate_gpu_scheduling_priority(
pub fn restore_gpu_scheduling_priority(process_id: Option<u32>) -> Result<()> {
gpu_priority::restore(process_id).map_err(napi::Error::from_reason)
}
#[napi(js_name = "registerVulkanLayerManifest")]
pub fn register_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
vulkan_layer_registry::register_manifest(&manifest_path).map_err(napi::Error::from_reason)
}
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
let _ = manifest_path;
Err(napi::Error::from_reason(
"Vulkan game capture layer is not included in this build",
))
}
}
#[napi(js_name = "unregisterVulkanLayerManifest")]
pub fn unregister_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
vulkan_layer_registry::unregister_manifest(&manifest_path).map_err(napi::Error::from_reason)
}
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
let _ = manifest_path;
Err(napi::Error::from_reason(
"Vulkan game capture layer is not included in this build",
))
}
}
#[napi(js_name = "getVulkanLayerRegistrationState")]
pub fn get_vulkan_layer_registration_state(manifest_path: String) -> VulkanLayerRegistrationState {
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let state = vulkan_layer_registry::registration_state(&manifest_path);
VulkanLayerRegistrationState {
registered: state.registered,
manifest_exists: state.manifest_exists,
dll_exists: state.dll_exists,
manifest_path: state.manifest_path,
}
}
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
VulkanLayerRegistrationState {
registered: false,
manifest_exists: false,
dll_exists: false,
manifest_path,
}
}
}
@@ -1,205 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::path::{Path, PathBuf};
use std::ptr::{null, null_mut};
use windows_sys::Win32::{
Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS},
System::Registry::{
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE, KEY_SET_VALUE, REG_DWORD,
REG_OPTION_NON_VOLATILE, REG_VALUE_TYPE, RegCloseKey, RegCreateKeyExW, RegDeleteValueW,
RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
},
};
const VULKAN_IMPLICIT_LAYERS_KEY: &str = "Software\\Khronos\\Vulkan\\ImplicitLayers";
const HKCU: HKEY = HKEY_CURRENT_USER;
const HKLM: HKEY = HKEY_LOCAL_MACHINE;
pub struct RegistrationState {
pub registered: bool,
pub manifest_exists: bool,
pub dll_exists: bool,
pub manifest_path: String,
}
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn manifest_dll_path(manifest_path: &Path) -> Option<PathBuf> {
let dir = manifest_path.parent()?;
if let Ok(contents) = std::fs::read_to_string(manifest_path)
&& let Some(library) = extract_library_path(&contents)
{
let candidate = Path::new(&library);
if candidate.is_absolute() {
return Some(candidate.to_path_buf());
}
return Some(dir.join(candidate));
}
None
}
fn extract_library_path(contents: &str) -> Option<String> {
let key = "\"library_path\"";
let key_pos = contents.find(key)?;
let rest = contents[key_pos + key.len()..].trim_start();
let rest = rest.strip_prefix(':')?.trim_start();
let rest = rest.strip_prefix('"')?;
let end = rest.find('"')?;
let raw = &rest[..end];
Some(raw.replace("\\\\", "\\"))
}
pub fn register_manifest(manifest_path: &str) -> Result<(), String> {
if manifest_path.trim().is_empty() {
return Err("Vulkan layer manifest path is empty".into());
}
let manifest = Path::new(manifest_path);
if !manifest.is_file() {
return Err(format!(
"Vulkan layer manifest does not exist: {}",
manifest.display()
));
}
if let Some(dll) = manifest_dll_path(manifest)
&& !dll.is_file()
{
return Err(format!(
"Vulkan layer DLL referenced by manifest does not exist: {}",
dll.display()
));
}
set_value_under(HKCU, manifest_path)?;
let _ = set_value_under(HKLM, manifest_path);
Ok(())
}
fn set_value_under(root: HKEY, manifest_path: &str) -> Result<(), String> {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let enabled: u32 = 0;
let mut key: HKEY = null_mut();
let create_status = unsafe {
RegCreateKeyExW(
root,
subkey.as_ptr(),
0,
null(),
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
null(),
&mut key,
null_mut(),
)
};
if create_status != ERROR_SUCCESS {
return Err(format!(
"RegCreateKeyExW Vulkan implicit layers failed: {create_status}"
));
}
let set_status = unsafe {
RegSetValueExW(
key,
value_name.as_ptr(),
0,
REG_DWORD,
(&enabled as *const u32).cast(),
std::mem::size_of::<u32>() as u32,
)
};
unsafe {
RegCloseKey(key);
}
if set_status != ERROR_SUCCESS {
return Err(format!(
"RegSetValueExW Vulkan implicit layer manifest failed: {set_status}"
));
}
Ok(())
}
pub fn unregister_manifest(manifest_path: &str) -> Result<(), String> {
if manifest_path.trim().is_empty() {
return Err("Vulkan layer manifest path is empty".into());
}
let hkcu = delete_value_under(HKCU, manifest_path);
let _ = delete_value_under(HKLM, manifest_path);
hkcu
}
fn delete_value_under(root: HKEY, manifest_path: &str) -> Result<(), String> {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let mut key: HKEY = null_mut();
let open_status = unsafe { RegOpenKeyExW(root, subkey.as_ptr(), 0, KEY_SET_VALUE, &mut key) };
if open_status == ERROR_FILE_NOT_FOUND {
return Ok(());
}
if open_status != ERROR_SUCCESS {
return Err(format!(
"RegOpenKeyExW Vulkan implicit layers failed: {open_status}"
));
}
let delete_status = unsafe { RegDeleteValueW(key, value_name.as_ptr()) };
unsafe {
RegCloseKey(key);
}
if delete_status == ERROR_SUCCESS || delete_status == ERROR_FILE_NOT_FOUND {
Ok(())
} else {
Err(format!(
"RegDeleteValueW Vulkan implicit layer manifest failed: {delete_status}"
))
}
}
pub fn registration_state(manifest_path: &str) -> RegistrationState {
let manifest = Path::new(manifest_path);
let manifest_exists = manifest.is_file();
let dll_exists = manifest_dll_path(manifest)
.map(|dll| dll.is_file())
.unwrap_or(false);
let registered = registry_value_present(manifest_path);
RegistrationState {
registered,
manifest_exists,
dll_exists,
manifest_path: manifest_path.to_string(),
}
}
fn registry_value_present(manifest_path: &str) -> bool {
if manifest_path.trim().is_empty() {
return false;
}
registry_value_present_under(HKCU, manifest_path)
|| registry_value_present_under(HKLM, manifest_path)
}
fn registry_value_present_under(root: HKEY, manifest_path: &str) -> bool {
let subkey = wide(VULKAN_IMPLICIT_LAYERS_KEY);
let value_name = wide(manifest_path);
let mut key: HKEY = null_mut();
let open_status = unsafe { RegOpenKeyExW(root, subkey.as_ptr(), 0, KEY_QUERY_VALUE, &mut key) };
if open_status != ERROR_SUCCESS {
return false;
}
let mut value_type: REG_VALUE_TYPE = 0;
let query_status = unsafe {
RegQueryValueExW(
key,
value_name.as_ptr(),
null(),
&mut value_type,
null_mut(),
null_mut(),
)
};
unsafe {
RegCloseKey(key);
}
query_status == ERROR_SUCCESS
}
@@ -1,277 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ash"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
[[package]]
name = "ash-layer"
version = "0.0.2+v0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedca11308198ffa300ad1a981c10ee176e44c8ff6ee7c6883a062608b9779a5"
dependencies = [
"ash",
]
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "fluxer_vulkan_layer"
version = "0.0.0"
dependencies = [
"ash",
"ash-layer",
"dashmap",
"once_cell",
"windows",
"windows-sys",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
@@ -1,31 +0,0 @@
[package]
name = "fluxer_vulkan_layer"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib"]
[target.'cfg(target_os = "windows")'.dependencies]
ash = {version = "0.38.0", default-features = false, features = ["std", "debug"]}
ash-layer = "0.0.2"
dashmap = "6.2.1"
once_cell = "1.21.4"
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
]}
windows-sys = {version = "0.61.2", features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_Threading",
]}
@@ -1,138 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use ash::vk;
use windows::Win32::Graphics::{
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN, D3D_DRIVER_TYPE_WARP},
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,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
},
Dxgi::{
Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
},
IDXGIResource,
},
};
use windows::core::Interface;
use crate::game_capture_abi::{GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_TEN_BIT};
#[derive(Clone, Copy)]
pub struct InteropFormat {
pub vk_format: vk::Format,
pub dxgi_format: DXGI_FORMAT,
pub capture_flags: u32,
}
pub fn interop_format(format: vk::Format) -> Option<InteropFormat> {
let (dxgi_format, capture_flags) = match format {
vk::Format::B8G8R8A8_UNORM => (DXGI_FORMAT_B8G8R8A8_UNORM, 0),
vk::Format::B8G8R8A8_SRGB => (DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, 0),
vk::Format::R8G8B8A8_UNORM => (DXGI_FORMAT_R8G8B8A8_UNORM, 0),
vk::Format::R8G8B8A8_SRGB => (DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, 0),
vk::Format::A2B10G10R10_UNORM_PACK32 => (
DXGI_FORMAT_R10G10B10A2_UNORM,
GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR,
),
vk::Format::R16G16B16A16_SFLOAT => (DXGI_FORMAT_R16G16B16A16_FLOAT, GAME_CAPTURE_FLAG_HDR),
_ => return None,
};
Some(InteropFormat {
vk_format: format,
dxgi_format,
capture_flags,
})
}
pub struct D3d11Device {
device: ID3D11Device,
_context: ID3D11DeviceContext,
}
unsafe impl Send for D3d11Device {}
unsafe impl Sync for D3d11Device {}
pub struct SharedTexture {
_texture: ID3D11Texture2D,
pub handle: u64,
}
unsafe impl Send for SharedTexture {}
unsafe impl Sync for SharedTexture {}
impl D3d11Device {
pub fn create() -> Option<Self> {
for driver in [D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP] {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let result = unsafe {
D3D11CreateDevice(
None,
driver,
Default::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
};
if result.is_ok()
&& let (Some(device), Some(context)) = (device, context)
{
return Some(Self {
device,
_context: context,
});
}
}
let _ = D3D_DRIVER_TYPE_UNKNOWN;
None
}
pub fn create_shared_texture(
&self,
width: u32,
height: u32,
format: InteropFormat,
) -> Option<SharedTexture> {
if width == 0 || height == 0 {
return None;
}
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: format.dxgi_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: (D3D11_BIND_SHADER_RESOURCE.0 | D3D11_BIND_RENDER_TARGET.0) as u32,
CPUAccessFlags: 0,
MiscFlags: D3D11_RESOURCE_MISC_SHARED.0 as u32,
};
let mut texture: Option<ID3D11Texture2D> = None;
let result = unsafe { self.device.CreateTexture2D(&desc, None, Some(&mut texture)) };
if result.is_err() {
return None;
}
let texture = texture?;
let resource: IDXGIResource = texture.cast().ok()?;
let handle = unsafe { resource.GetSharedHandle() }.ok()?;
if handle.is_invalid() {
return None;
}
Some(SharedTexture {
_texture: texture,
handle: handle.0 as usize as u64,
})
}
}
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -472,8 +472,6 @@ export interface NativeScreenCaptureSource {
targetPid?: number;
}
export type GameCaptureInjectionMethod = 'auto' | 'remote-thread' | 'set-windows-hook';
export interface NativeScreenCaptureRect {
x: number;
y: number;
@@ -487,7 +485,6 @@ export interface NativeScreenCaptureStartOptions {
width?: number;
height?: number;
frameRate?: number;
injectionMethod?: GameCaptureInjectionMethod;
captureId?: string;
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
@@ -523,7 +520,7 @@ export interface NativeScreenCaptureLifecycleMessage {
source?: NativeScreenCaptureLifecycleSource;
}
export type NativeScreenCaptureStrategy = 'game-hook' | 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export type NativeScreenCaptureStrategy = 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export interface NativeScreenCaptureDiagnostics {
state?: number;
@@ -538,8 +535,6 @@ export interface NativeScreenCaptureDiagnostics {
droppedFrameCounter?: number;
lastPresentTimestampUs?: number;
lastError?: number;
requestedInjectionMethod?: string;
injectionMethod?: string;
activeStrategy?: NativeScreenCaptureStrategy;
lastFallbackReason?: string;
backend?: string;
@@ -273,7 +273,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 2560,
height: 1440,
frameRate: 60,
injectionMethod: undefined,
captureId: 'capture-1',
colorRange: 'full',
colorSpace: 'rec709',
@@ -287,7 +286,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 1280,
height: 720,
frameRate: 30,
injectionMethod: undefined,
captureId: 'capture-2',
colorRange: undefined,
colorSpace: undefined,
@@ -352,7 +350,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 1280,
height: 720,
frameRate: 30,
injectionMethod: undefined,
captureId: 'preselected-capture-id',
colorRange: undefined,
colorSpace: undefined,
@@ -485,14 +482,12 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 2560,
height: 1440,
frameRate: 60,
injectionMethod: 'set-windows-hook',
nativeFrameSinkRequired: true,
},
);
assert.equal(captures.length, 1);
assert.equal(captures[0].options.sourceKind, 'screen');
assert.equal(captures[0].options.injectionMethod, undefined);
const diagnostics = await harness.handlers.get('native-screen-capture:get-diagnostics')({sender}, result.captureId);
assert.equal(diagnostics.sourceKind, 'screen');
@@ -827,7 +827,6 @@ async function startNativeScreenCapture(
width: requestedWidth,
height: requestedHeight,
frameRate: options.frameRate ?? 30,
injectionMethod: options.sourceKind === 'game' ? options.injectionMethod : undefined,
captureId,
colorRange: options.colorRange,
colorSpace: options.colorSpace,
@@ -86,14 +86,6 @@ export function isValidStartOptions(options: unknown): options is NativeScreenCa
) {
return false;
}
if (
options.injectionMethod !== undefined &&
options.injectionMethod !== 'auto' &&
options.injectionMethod !== 'remote-thread' &&
options.injectionMethod !== 'set-windows-hook'
) {
return false;
}
if (
options.captureId !== undefined &&
(typeof options.captureId !== 'string' ||
@@ -1,165 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync} from 'node:child_process';
import {createRequire} from 'node:module';
import log from 'electron-log';
const requireModule = createRequire(import.meta.url);
const VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY = 'Software\\Khronos\\Vulkan\\ImplicitLayers';
const VULKAN_REGISTRY_ROOTS = ['HKCU', 'HKLM'] as const;
const FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME = /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/;
const FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES = new Set(['win-game-capture', 'win-screen-capture']);
interface VulkanLayerRegistrationState {
registered: boolean;
manifestExists: boolean;
dllExists: boolean;
manifestPath: string | null;
}
type WindowsGameCaptureModule = {
loadError?: Error | null;
isGameCaptureHookAvailable?: () => boolean;
registerVulkanLayerManifest?: () => boolean;
unregisterVulkanLayerManifest?: () => boolean;
resolveVulkanLayerManifestPath?: () => string | null;
getVulkanLayerRegistrationState?: () => VulkanLayerRegistrationState;
};
function parseRegistryValueNames(stdout: string): Array<string> {
const valueNames: Array<string> = [];
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
const match = trimmed.match(/^(.*?)\s+REG_DWORD\s+(?:0x[0-9a-f]+|\d+)$/i);
if (!match) continue;
const valueName = match[1].trim();
if (valueName.length > 0) valueNames.push(valueName);
}
return valueNames;
}
function normalizeVulkanLayerValueName(valueName: string): string {
return valueName.replace(/\//g, '\\').toLowerCase();
}
export function isFluxerGameCaptureVulkanLayerValue(valueName: string): boolean {
const segments = normalizeVulkanLayerValueName(valueName).split('\\');
const fileName = segments.at(-1) ?? '';
const packageDirectoryName = segments.at(-2) ?? '';
if (!FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME.test(fileName)) return false;
return FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES.has(packageDirectoryName);
}
function queryVulkanLayerRegistryValues(root: string): Array<string> {
try {
const stdout = execFileSync('reg.exe', ['query', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`], {
encoding: 'utf8',
windowsHide: true,
});
return parseRegistryValueNames(stdout);
} catch (error) {
const status = (error as {status?: number} | null)?.status;
if (status === 1) return [];
throw error;
}
}
function deleteVulkanLayerRegistryValue(root: string, valueName: string): void {
execFileSync('reg.exe', ['delete', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`, '/v', valueName, '/f'], {
stdio: 'ignore',
windowsHide: true,
});
}
function isSameVulkanLayerManifestPath(left: string, right: string): boolean {
return normalizeVulkanLayerValueName(left) === normalizeVulkanLayerValueName(right);
}
export function shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(
valueName: string,
keepManifestPath: string | null,
): boolean {
if (!isFluxerGameCaptureVulkanLayerValue(valueName)) return false;
return keepManifestPath === null || !isSameVulkanLayerManifestPath(valueName, keepManifestPath);
}
function removeStaleFluxerGameCaptureVulkanLayers(keepManifestPath: string | null): void {
if (process.platform !== 'win32') return;
for (const root of VULKAN_REGISTRY_ROOTS) {
let valueNames: Array<string>;
try {
valueNames = queryVulkanLayerRegistryValues(root);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to enumerate Vulkan implicit layer registry values', {root, error});
continue;
}
for (const valueName of valueNames) {
if (!shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(valueName, keepManifestPath)) continue;
try {
deleteVulkanLayerRegistryValue(root, valueName);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to remove stale Fluxer Vulkan layer registry value', {
root,
valueName,
error,
});
continue;
}
log.info('[VulkanGameCaptureLayer] Removed stale Fluxer Vulkan layer registry value', {root, valueName});
}
}
}
function loadWindowsGameCaptureModule(): WindowsGameCaptureModule | null {
if (process.platform !== 'win32') return null;
try {
const addon = requireModule('@fluxer/win-game-capture') as WindowsGameCaptureModule;
if (addon.loadError) {
log.warn('[VulkanGameCaptureLayer] Native game capture addon unavailable', addon.loadError);
return null;
}
return addon;
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to load the native game capture addon', error);
return null;
}
}
export function initializeWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
if (!addon || addon.isGameCaptureHookAvailable?.() !== true) {
removeStaleFluxerGameCaptureVulkanLayers(null);
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer left unregistered; hook-based game capture is disabled');
return;
}
try {
const manifestPath = addon.resolveVulkanLayerManifestPath?.() ?? null;
removeStaleFluxerGameCaptureVulkanLayers(manifestPath);
const registered = addon.registerVulkanLayerManifest?.() ?? false;
const state = addon.getVulkanLayerRegistrationState?.() ?? null;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer registration checked', {
registered,
manifestPath,
state,
});
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to register Vulkan implicit layer', error);
}
}
export function unregisterWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
try {
const unregistered = addon?.unregisterVulkanLayerManifest?.() ?? false;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer unregistration attempted', {
unregistered,
manifestPath: addon?.resolveVulkanLayerManifestPath?.() ?? null,
});
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to unregister Vulkan implicit layer', error);
}
removeStaleFluxerGameCaptureVulkanLayers(null);
}
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync} from 'node:child_process';
import log from 'electron-log';
const VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY = 'Software\\Khronos\\Vulkan\\ImplicitLayers';
const VULKAN_REGISTRY_ROOTS = ['HKCU', 'HKLM'] as const;
const FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME = /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/;
const FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES = new Set(['win-game-capture', 'win-screen-capture']);
function parseRegistryValueNames(stdout: string): Array<string> {
const valueNames: Array<string> = [];
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
const match = trimmed.match(/^(.*?)\s+REG_DWORD\s+(?:0x[0-9a-f]+|\d+)$/i);
if (!match) continue;
const valueName = match[1].trim();
if (valueName.length > 0) valueNames.push(valueName);
}
return valueNames;
}
function normalizeVulkanLayerValueName(valueName: string): string {
return valueName.replace(/\//g, '\\').toLowerCase();
}
function isFluxerVulkanLayerValue(valueName: string): boolean {
const segments = normalizeVulkanLayerValueName(valueName).split('\\');
const fileName = segments.at(-1) ?? '';
const packageDirectoryName = segments.at(-2) ?? '';
if (!FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME.test(fileName)) return false;
return FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES.has(packageDirectoryName);
}
function queryVulkanLayerRegistryValues(root: string): Array<string> {
try {
const stdout = execFileSync('reg.exe', ['query', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`], {
encoding: 'utf8',
windowsHide: true,
});
return parseRegistryValueNames(stdout);
} catch (error) {
const status = (error as {status?: number} | null)?.status;
if (status === 1) return [];
throw error;
}
}
function deleteVulkanLayerRegistryValue(root: string, valueName: string): void {
execFileSync('reg.exe', ['delete', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`, '/v', valueName, '/f'], {
stdio: 'ignore',
windowsHide: true,
});
}
export function removeFluxerVulkanLayerRegistrations(): void {
if (process.platform !== 'win32') return;
for (const root of VULKAN_REGISTRY_ROOTS) {
let valueNames: Array<string>;
try {
valueNames = queryVulkanLayerRegistryValues(root);
} catch (error) {
log.warn('[VulkanLayerCleanup] Failed to enumerate Vulkan implicit layer registry values', {root, error});
continue;
}
for (const valueName of valueNames) {
if (!isFluxerVulkanLayerValue(valueName)) continue;
try {
deleteVulkanLayerRegistryValue(root, valueName);
} catch (error) {
log.warn('[VulkanLayerCleanup] Failed to remove Fluxer Vulkan layer registry value', {
root,
valueName,
error,
});
continue;
}
log.info('[VulkanLayerCleanup] Removed Fluxer Vulkan layer registry value', {root, valueName});
}
}
}
+3 -3
View File
@@ -78,7 +78,7 @@ import {
setQuitting,
showWindow,
} from '@electron/main/Window';
import {initializeWindowsVulkanGameCaptureLayer} from '@electron/main/WindowsVulkanGameCaptureLayer';
import {removeFluxerVulkanLayerRegistrations} from '@electron/main/WindowsVulkanLayerCleanup';
import {app, dialog, netLog} from 'electron';
import log from 'electron-log';
@@ -378,9 +378,9 @@ if (launchConfigurationError) {
log.error('[Init] Failed to register native audio handlers:', error);
}
try {
runStartupPhase('vulkan-game-capture-layer', initializeWindowsVulkanGameCaptureLayer);
runStartupPhase('vulkan-layer-cleanup', removeFluxerVulkanLayerRegistrations);
} catch (error: unknown) {
log.error('[Init] Failed to initialize Vulkan game capture layer:', error);
log.error('[Init] Failed to remove stale Vulkan layer registrations:', error);
}
try {
runStartupPhase('native-screen-capture-handlers', registerNativeScreenCaptureHandlers);
-1
View File
@@ -23,7 +23,6 @@
"fluxer_desktop/src/main/Autostart.ts": ["exports"],
"fluxer_desktop/src/main/LinuxDesktopEntry.ts": ["exports"],
"fluxer_desktop/src/main/NotificationState.ts": ["exports", "types"],
"fluxer_desktop/src/main/WindowsVulkanGameCaptureLayer.ts": ["exports"],
"packages/schema/src/domains/geolocation/GeolocationSchemas.ts": ["exports"],
"pnpm-workspace.yaml": ["catalog"]
},
+48 -44
View File
@@ -365,9 +365,6 @@ fn build_rust_node_addon_for_arch(
OsString::from("--manifest-path"),
OsString::from("Cargo.toml"),
];
if addon.special == DesktopNativeSpecialBuild::WinGameCapture {
args.push(OsString::from("--no-default-features"));
}
if !addon.features.is_empty() {
args.push(OsString::from("--features"));
args.push(OsString::from(addon.features.join(",")));
@@ -416,6 +413,7 @@ fn build_rust_node_addon_for_arch(
);
sign_macos_node_addon(&out_file, platform)?;
assert_no_redistributable_runtime_imports(&out_file, platform)?;
assert_system32_dependent_load_flag(&out_file, platform)?;
assert_no_disabled_win_game_capture_capabilities(&out_file, addon, platform)?;
Ok(BuiltNodeAddon {
out_file,
@@ -708,26 +706,6 @@ fn remove_stale_win_game_capture_outputs(root: &Path, primary_node: &Path) -> Re
Ok(())
}
#[cfg(test)]
fn vulkan_layer_manifest(layer_dll_name: &str) -> String {
format!(
"{{\n\
\t\"file_format_version\": \"1.2.0\",\n\
\t\"layer\": {{\n\
\t\t\"name\": \"VK_LAYER_FLUXER_game_capture\",\n\
\t\t\"type\": \"GLOBAL\",\n\
\t\t\"library_path\": \".\\\\\\\\{layer_dll_name}\",\n\
\t\t\"api_version\": \"1.0.0\",\n\
\t\t\"implementation_version\": \"1\",\n\
\t\t\"description\": \"Fluxer Vulkan game capture layer\",\n\
\t\t\"disable_environment\": {{\n\
\t\t\t\"DISABLE_FLUXER_VULKAN_CAPTURE\": \"\"\n\
\t\t}}\n\
\t}}\n\
}}\n"
)
}
fn assert_no_redistributable_runtime_imports(node_file_path: &Path, platform: &str) -> Result<()> {
if platform != "win32" {
return Ok(());
@@ -756,6 +734,44 @@ const DISABLED_WIN_GAME_CAPTURE_BINARY_MARKERS: &[&[u8]] = &[
b"fluxer-inject-helper.",
];
const LOAD_LIBRARY_SEARCH_SYSTEM32: u16 = 0x0800;
fn read_dependent_load_flags(file_path: &Path) -> Option<u16> {
let buffer = fs::read(file_path).ok()?;
let pe = PeFile { buffer };
let header = pe.parse_header()?;
if header.load_config_directory_rva == 0 {
return None;
}
let offset = pe.rva_to_offset(&header.sections, header.load_config_directory_rva)?;
if offset + 4 > pe.buffer.len() {
return None;
}
let size = pe.u32(offset) as usize;
let flags_offset = if header.is_pe32_plus { 0x42 } else { 0x36 };
if size <= flags_offset + 2 || offset + flags_offset + 2 > pe.buffer.len() {
return None;
}
Some(pe.u16(offset + flags_offset))
}
fn assert_system32_dependent_load_flag(node_file_path: &Path, platform: &str) -> Result<()> {
if platform != "win32" {
return Ok(());
}
let flags = read_dependent_load_flags(node_file_path);
ensure!(
flags.is_some_and(|value| value & LOAD_LIBRARY_SEARCH_SYSTEM32 != 0),
"{} does not set LOAD_LIBRARY_SEARCH_SYSTEM32 in its load config DependentLoadFlags (read {}).\nEvery import of a shipped addon must resolve from System32 so a planted DLL cannot be loaded in its place.\nSee fluxer_desktop/native/.cargo/config.toml for the /DEPENDENTLOADFLAG:0x800 link argument that sets it.",
node_file_path.display(),
flags.map_or_else(
|| "no load config".to_string(),
|value| format!("{value:#06x}")
)
);
Ok(())
}
fn assert_no_disabled_win_game_capture_capabilities(
node_file_path: &Path,
addon: &DesktopNativeAddon,
@@ -825,6 +841,8 @@ struct PeFile {
struct PeHeader {
sections: Vec<PeSection>,
import_directory_rva: u32,
load_config_directory_rva: u32,
is_pe32_plus: bool,
}
#[derive(Debug)]
@@ -900,6 +918,11 @@ impl PeFile {
return None;
}
let import_directory_rva = self.u32(import_directory_entry_offset);
let load_config_directory_entry_offset = data_directories_offset + 10 * 8;
if load_config_directory_entry_offset + 8 > self.buffer.len() {
return None;
}
let load_config_directory_rva = self.u32(load_config_directory_entry_offset);
let section_table_offset = optional_header_offset + size_of_optional_header;
let mut sections = Vec::new();
for index in 0..number_of_sections {
@@ -917,6 +940,8 @@ impl PeFile {
Some(PeHeader {
sections,
import_directory_rva,
load_config_directory_rva,
is_pe32_plus,
})
}
@@ -1024,25 +1049,4 @@ mod tests {
assert!(!should_bundle_linux_library("ld-linux-x86-64.so.2"));
assert!(should_bundle_linux_library("libfido2.so.1"));
}
#[test]
fn vulkan_layer_manifest_matches_legacy_json_shape() {
assert_eq!(
vulkan_layer_manifest("fluxer-vulkan-layer.win32-x64-msvc.dll"),
"{\n\
\t\"file_format_version\": \"1.2.0\",\n\
\t\"layer\": {\n\
\t\t\"name\": \"VK_LAYER_FLUXER_game_capture\",\n\
\t\t\"type\": \"GLOBAL\",\n\
\t\t\"library_path\": \".\\\\\\\\fluxer-vulkan-layer.win32-x64-msvc.dll\",\n\
\t\t\"api_version\": \"1.0.0\",\n\
\t\t\"implementation_version\": \"1\",\n\
\t\t\"description\": \"Fluxer Vulkan game capture layer\",\n\
\t\t\"disable_environment\": {\n\
\t\t\t\"DISABLE_FLUXER_VULKAN_CAPTURE\": \"\"\n\
\t\t}\n\
\t}\n\
}\n"
);
}
}