mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
fix(desktop): drop game capture injection and pin dll search (#2069)
This commit is contained in:
-7
@@ -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':
|
||||
|
||||
-13
@@ -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)}
|
||||
|
||||
-11
@@ -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'},
|
||||
];
|
||||
|
||||
-1
@@ -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';
|
||||
|
||||
Vendored
+1
-6
@@ -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`),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user