diff --git a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts index d34a8e5b4..09141f4d4 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts +++ b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts @@ -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> = { mockAttachmentStates: ATTACHMENT_MOCKS_DESCRIPTOR, }; @@ -327,7 +323,6 @@ const formatDeveloperOptionValue = ( 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': diff --git a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/GeneralOptionsMenu.tsx b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/GeneralOptionsMenu.tsx index a37a5d2ab..5d1665028 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/GeneralOptionsMenu.tsx +++ b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/GeneralOptionsMenu.tsx @@ -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 && ( - - )} {canConfigureMentionSuppression && ( -> => [ - {value: 'auto', label: INJECT_METHOD_AUTOMATIC_DESCRIPTOR}, - {value: 'remote-thread', label: 'CreateRemoteThread'}, - {value: 'set-windows-hook', label: 'SetWindowsHookEx'}, -]; diff --git a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts index 72221a316..aa536459b 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts +++ b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts @@ -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([ 'premiumTypeOverride', diff --git a/fluxer_app/src/features/devtools/state/DeveloperOptions.ts b/fluxer_app/src/features/devtools/state/DeveloperOptions.ts index 599de7e7b..9d2a5bd83 100644 --- a/fluxer_app/src/features/devtools/state/DeveloperOptions.ts +++ b/fluxer_app/src/features/devtools/state/DeveloperOptions.ts @@ -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', ]); } diff --git a/fluxer_app/src/features/voice/engine/ScreenShareCaptureDiagnostics.ts b/fluxer_app/src/features/voice/engine/ScreenShareCaptureDiagnostics.ts index 14514b721..c96e24d75 100644 --- a/fluxer_app/src/features/voice/engine/ScreenShareCaptureDiagnostics.ts +++ b/fluxer_app/src/features/voice/engine/ScreenShareCaptureDiagnostics.ts @@ -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'; diff --git a/fluxer_app/src/types/electron.d.ts b/fluxer_app/src/types/electron.d.ts index 6a06012b5..443e41f64 100644 --- a/fluxer_app/src/types/electron.d.ts +++ b/fluxer_app/src/types/electron.d.ts @@ -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; diff --git a/fluxer_desktop/electron-builder.config.cjs b/fluxer_desktop/electron-builder.config.cjs index c00a19302..72254cf54 100644 --- a/fluxer_desktop/electron-builder.config.cjs +++ b/fluxer_desktop/electron-builder.config.cjs @@ -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`), ]); } diff --git a/fluxer_desktop/native/.cargo/config.toml b/fluxer_desktop/native/.cargo/config.toml index 4d136d540..a30e57356 100644 --- a/fluxer_desktop/native/.cargo/config.toml +++ b/fluxer_desktop/native/.cargo/config.toml @@ -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"] diff --git a/fluxer_desktop/native/win-game-capture/Cargo.toml b/fluxer_desktop/native/win-game-capture/Cargo.toml index 4bc257cec..82a7de508 100644 --- a/fluxer_desktop/native/win-game-capture/Cargo.toml +++ b/fluxer_desktop/native/win-game-capture/Cargo.toml @@ -8,10 +8,6 @@ publish = false [workspace] resolver = "2" -[features] -default = [] -game-capture-hook = [] - [lib] crate-type = ["cdylib", "rlib"] diff --git a/fluxer_desktop/native/win-game-capture/compatibility.json b/fluxer_desktop/native/win-game-capture/compatibility.json deleted file mode 100644 index 1fcd8ff60..000000000 --- a/fluxer_desktop/native/win-game-capture/compatibility.json +++ /dev/null @@ -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": [] -} diff --git a/fluxer_desktop/native/win-game-capture/hook/Cargo.lock b/fluxer_desktop/native/win-game-capture/hook/Cargo.lock deleted file mode 100644 index e66158b95..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/Cargo.lock +++ /dev/null @@ -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" diff --git a/fluxer_desktop/native/win-game-capture/hook/Cargo.toml b/fluxer_desktop/native/win-game-capture/hook/Cargo.toml deleted file mode 100644 index 868ac045c..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/Cargo.toml +++ /dev/null @@ -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", -]} diff --git a/fluxer_desktop/native/win-game-capture/hook/src/arm64_reloc.rs b/fluxer_desktop/native/win-game-capture/hook/src/arm64_reloc.rs deleted file mode 100644 index f416abdd5..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/src/arm64_reloc.rs +++ /dev/null @@ -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 { - 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 { - 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, 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 { - 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 { - if (insn & 0xFFFF_FC1F) != 0xD61F_0000 { - return None; - } - Some((insn >> 5) & 0x1F) -} - -pub unsafe fn import_thunk_target(prologue: &[u8], src_base: u64) -> Option { - 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 { - 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> { - 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 { - 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 { - 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 { - 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 { - 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> { - 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 = Vec::with_capacity(count * 4); - let mut islands_out: Vec = 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> { - 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()) -} diff --git a/fluxer_desktop/native/win-game-capture/hook/src/d3d9.rs b/fluxer_desktop/native/win-game-capture/hook/src/d3d9.rs deleted file mode 100644 index 3cb139159..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/src/d3d9.rs +++ /dev/null @@ -1,1377 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::{ - D3d11FormatMode, Detour, ENV_FORCE_CPU, GAME_CAPTURE_API_D3D9, - GAME_CAPTURE_FALLBACK_DEVICE_LOST, GAME_CAPTURE_FALLBACK_FORCED_CPU, - GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED, GAME_CAPTURE_FALLBACK_NONE, - GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED, HOOKS_READY, HookState, capture_should_run, - clear_state, ensure_state, env_flag_enabled, frame_due, mark_present, - publish_shared_texture_frame, record_dropped_frame, set_capture_flags, set_fallback_reason, - verbose_log, wide, write_bgra_rows_to_shared_memory, -}; -use std::{ - ffi::c_void, - mem, - ptr::null_mut, - sync::atomic::{AtomicBool, Ordering}, -}; -use windows::{ - Win32::{ - Foundation::HMODULE as WinHmodule, - Foundation::{HWND as WinHwnd, RECT}, - Graphics::Direct3D::{ - D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_11_0, - }, - Graphics::{ - Direct3D9::{ - D3D_SDK_VERSION, D3DADAPTER_DEFAULT, D3DBACKBUFFER_TYPE_MONO, - D3DCREATE_FPU_PRESERVE, D3DCREATE_HARDWARE_VERTEXPROCESSING, - D3DCREATE_MULTITHREADED, D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DDEVTYPE_HAL, - D3DDISPLAYMODEEX, D3DFMT_A8B8G8R8, D3DFMT_A8R8G8B8, D3DFMT_X8B8G8R8, - D3DFMT_X8R8G8B8, D3DFORMAT, D3DLOCK_READONLY, D3DLOCKED_RECT, D3DMULTISAMPLE_NONE, - D3DPOOL_DEFAULT, D3DPOOL_SYSTEMMEM, D3DPRESENT_PARAMETERS, D3DSURFACE_DESC, - D3DSWAPEFFECT_DISCARD, D3DTEXF_NONE, D3DUSAGE_RENDERTARGET, Direct3DCreate9, - IDirect3DDevice9, IDirect3DSurface9, IDirect3DSwapChain9, IDirect3DTexture9, - }, - Direct3D11::{ - D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_RESOURCE_MISC_SHARED, - D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11CreateDevice, - ID3D11Device, ID3D11Texture2D, - }, - Dxgi::{ - Common::{ - DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_SAMPLE_DESC, - }, - CreateDXGIFactory1, IDXGIFactory1, IDXGIResource, - }, - Gdi::RGNDATA, - }, - }, - core::Interface, -}; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use windows_sys::Win32::System::Memory::{ - MEM_COMMIT, MEMORY_BASIC_INFORMATION, PAGE_GUARD, PAGE_NOACCESS, VirtualQuery, -}; -use windows_sys::Win32::{ - Foundation::HWND, - UI::WindowsAndMessaging::{ - CW_USEDEFAULT, CreateWindowExW, DestroyWindow, GetClientRect, IsWindow, WS_OVERLAPPEDWINDOW, - }, -}; - -const VT_DEVICE9_RESET: usize = 16; -const VT_DEVICE9_PRESENT: usize = 17; -const VT_DEVICE9EX_PRESENTEX: usize = 121; -const VT_DEVICE9EX_CHECK_RESOURCE_RESIDENCY: usize = 125; -const VT_DEVICE9EX_RESETEX: usize = 132; -const VT_SWAPCHAIN9_PRESENT: usize = 3; - -type D3d9PresentFn = unsafe extern "system" fn( - *mut c_void, - *const RECT, - *const RECT, - WinHwnd, - *const RGNDATA, -) -> windows::core::HRESULT; -type D3d9PresentExFn = unsafe extern "system" fn( - *mut c_void, - *const RECT, - *const RECT, - WinHwnd, - *const RGNDATA, - u32, -) -> windows::core::HRESULT; -type D3d9ResetFn = - unsafe extern "system" fn(*mut c_void, *mut D3DPRESENT_PARAMETERS) -> windows::core::HRESULT; -type D3d9ResetExFn = unsafe extern "system" fn( - *mut c_void, - *mut D3DPRESENT_PARAMETERS, - *mut D3DDISPLAYMODEEX, -) -> windows::core::HRESULT; -type D3d9SwapChainPresentFn = unsafe extern "system" fn( - *mut c_void, - *const RECT, - *const RECT, - WinHwnd, - *const RGNDATA, - u32, -) -> windows::core::HRESULT; - -static mut D3D9_PRESENT_HOOK: Option> = None; -static mut D3D9_PRESENT_EX_HOOK: Option> = None; -static mut D3D9_RESET_HOOK: Option> = None; -static mut D3D9_RESET_EX_HOOK: Option> = None; -static mut D3D9_SWAPCHAIN_PRESENT_HOOK: Option> = None; -static D3D9_FIRED: AtomicBool = AtomicBool::new(false); -static D3D9_PRESENT_RECURSE: std::sync::atomic::AtomicUsize = - std::sync::atomic::AtomicUsize::new(0); - -pub(crate) struct D3d9CaptureState { - device_ptr: usize, - width: u32, - height: u32, - format: D3DFORMAT, - multisample_type: windows::Win32::Graphics::Direct3D9::D3DMULTISAMPLE_TYPE, - path: D3d9CapturePath, -} - -enum D3d9CapturePath { - SharedTexture { - copy_surface: IDirect3DSurface9, - _copy_texture: IDirect3DTexture9, - _d3d11_device: ID3D11Device, - _d3d11_texture: ID3D11Texture2D, - shared_handle: u64, - dxgi_format: DXGI_FORMAT, - }, - Memory { - surface: IDirect3DSurface9, - resolve_surface: Option, - format_mode: D3d11FormatMode, - }, -} - -pub(crate) unsafe fn hooks_installed() -> bool { - D3D9_PRESENT_HOOK.is_some() -} - -fn d3d9_format_mode(format: D3DFORMAT) -> Option { - if format == D3DFMT_X8R8G8B8 || format == D3DFMT_A8R8G8B8 { - Some(D3d11FormatMode::Bgra) - } else if format == D3DFMT_X8B8G8R8 || format == D3DFMT_A8B8G8R8 { - Some(D3d11FormatMode::Rgba) - } else { - None - } -} - -fn d3d9_shared_dxgi_format(format: D3DFORMAT) -> Option { - use windows::Win32::Graphics::Direct3D9::D3DFMT_A2B10G10R10; - if format == D3DFMT_A8R8G8B8 { - Some(DXGI_FORMAT_B8G8R8A8_UNORM) - } else if format == D3DFMT_X8R8G8B8 { - Some(DXGI_FORMAT_B8G8R8X8_UNORM) - } else if format == D3DFMT_A8B8G8R8 { - Some(DXGI_FORMAT_R8G8B8A8_UNORM) - } else if format == D3DFMT_A2B10G10R10 { - Some(DXGI_FORMAT_R10G10B10A2_UNORM) - } else { - None - } -} - -fn d3d9_hdr_flags(format: D3DFORMAT) -> u32 { - use crate::{GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_TEN_BIT}; - use windows::Win32::Graphics::Direct3D9::{ - D3DFMT_A2B10G10R10, D3DFMT_A2B10G10R10_XR_BIAS, D3DFMT_A2R10G10B10, D3DFMT_A16B16G16R16, - D3DFMT_A16B16G16R16F, - }; - if format == D3DFMT_A2R10G10B10 - || format == D3DFMT_A2B10G10R10 - || format == D3DFMT_A2B10G10R10_XR_BIAS - { - GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR - } else if format == D3DFMT_A16B16G16R16F || format == D3DFMT_A16B16G16R16 { - GAME_CAPTURE_FLAG_HDR - } else { - 0 - } -} - -#[derive(Clone, Copy, Debug)] -struct D3d9ExFlagOffsets { - d3d9_object_offset: usize, - is_d3d9ex_offset: usize, -} - -struct D3d9ExFlagGuard { - flag: *mut i32, - previous: i32, -} - -impl Drop for D3d9ExFlagGuard { - fn drop(&mut self) { - unsafe { - if !self.flag.is_null() { - *self.flag = self.previous; - } - } - } -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -static D3D9EX_FLAG_OFFSETS: std::sync::OnceLock> = - std::sync::OnceLock::new(); - -#[cfg(target_arch = "x86_64")] -const D3D9EX_PATTERN_LEN: usize = 22; -#[cfg(target_arch = "x86_64")] -const D3D9EX_PATTERN_MASKS: [[u8; D3D9EX_PATTERN_LEN]; 4] = [ - [ - 0xF8, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, - 0x00, 0xF8, 0xF8, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xF8, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, - 0xF8, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xF8, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, - 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xF8, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, - 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, - ], -]; -#[cfg(target_arch = "x86_64")] -const D3D9EX_PATTERN_VALUES: [[u8; D3D9EX_PATTERN_LEN]; 4] = [ - [ - 0x48, 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x44, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, - 0x00, 0x40, 0xB8, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x48, 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x39, 0x80, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, - 0x40, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x48, 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x39, 0x80, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, - 0x48, 0x8D, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x48, 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x83, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, - 0x00, 0x48, 0x8D, 0x00, 0x00, 0x00, 0x00, - ], -]; -#[cfg(target_arch = "x86_64")] -const D3D9EX_OFFSET_FIELDS: [(usize, usize); 4] = [(3, 10), (3, 9), (3, 9), (3, 9)]; - -#[cfg(target_arch = "x86")] -const D3D9EX_PATTERN_LEN: usize = 20; -#[cfg(target_arch = "x86")] -const D3D9EX_PATTERN_MASKS: [[u8; D3D9EX_PATTERN_LEN]; 4] = [ - [ - 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, - 0xFF, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, - 0xFF, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x00, - ], -]; -#[cfg(target_arch = "x86")] -const D3D9EX_PATTERN_VALUES: [[u8; D3D9EX_PATTERN_LEN]; 4] = [ - [ - 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x39, 0x80, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, 0x68, - 0x00, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x83, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, - 0x68, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x83, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, - 0xBA, 0x00, 0x00, 0x00, 0x00, - ], - [ - 0x8B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x39, 0x80, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, 0xBA, - 0x00, 0x00, 0x00, 0x00, 0x00, - ], -]; -#[cfg(target_arch = "x86")] -const D3D9EX_OFFSET_FIELDS: [(usize, usize); 4] = [(2, 8), (2, 8), (2, 8), (2, 8)]; - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -fn d3d9ex_pattern_offsets(bytes: &[u8]) -> Option<(usize, usize)> { - if bytes.len() < D3D9EX_PATTERN_LEN { - return None; - } - for (index, (mask, value)) in D3D9EX_PATTERN_MASKS - .iter() - .zip(D3D9EX_PATTERN_VALUES.iter()) - .enumerate() - { - if bytes[..D3D9EX_PATTERN_LEN] - .iter() - .zip(mask.iter()) - .zip(value.iter()) - .all(|((byte, mask), value)| (*byte & *mask) == *value) - { - return Some(D3D9EX_OFFSET_FIELDS[index]); - } - } - None -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -unsafe fn memory_range_readable(ptr: *const u8, len: usize) -> bool { - if ptr.is_null() || len == 0 { - return false; - } - let mut info: MEMORY_BASIC_INFORMATION = mem::zeroed(); - let queried = VirtualQuery( - ptr.cast(), - &mut info, - mem::size_of::(), - ); - if queried == 0 || info.State != MEM_COMMIT { - return false; - } - if (info.Protect & PAGE_NOACCESS) != 0 || (info.Protect & PAGE_GUARD) != 0 { - return false; - } - let start = ptr as usize; - let region_start = info.BaseAddress as usize; - let Some(end) = start.checked_add(len) else { - return false; - }; - let region_end = region_start.saturating_add(info.RegionSize); - start >= region_start && end <= region_end -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -unsafe fn read_u32_field(base: *const u8, offset: usize) -> u32 { - std::ptr::read_unaligned(base.add(offset).cast::()) -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -unsafe fn validate_d3d9ex_offsets( - device_raw: *mut c_void, - d3d9ex_raw: *mut c_void, - offsets: D3d9ExFlagOffsets, -) -> bool { - if offsets.d3d9_object_offset > 0xFFFF || offsets.is_d3d9ex_offset > 0xFFFF { - return false; - } - let device = device_raw.cast::(); - let d3d9_object_slot = device.add(offsets.d3d9_object_offset).cast::<*mut u8>(); - if !memory_range_readable(d3d9_object_slot.cast(), mem::size_of::<*mut u8>()) { - return false; - } - let d3d9_object = *d3d9_object_slot; - if d3d9_object != d3d9ex_raw.cast::() { - return false; - } - let flag = d3d9_object.add(offsets.is_d3d9ex_offset).cast::(); - if !memory_range_readable(flag.cast(), mem::size_of::()) { - return false; - } - *flag == 1 -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -unsafe fn discover_d3d9ex_flag_offsets() -> Option { - use windows::Win32::Graphics::Direct3D9::{Direct3DCreate9Ex, IDirect3DDevice9Ex}; - - let Ok(d3d9ex) = Direct3DCreate9Ex(D3D_SDK_VERSION) else { - verbose_log("d3d9 shared texture: Direct3DCreate9Ex unavailable for offset discovery"); - return None; - }; - let hwnd = create_dummy_window_d3d9(); - if hwnd.is_null() { - verbose_log("d3d9 shared texture: dummy window creation failed for offset discovery"); - return None; - } - let mut params = D3DPRESENT_PARAMETERS { - Windowed: windows::core::BOOL(1), - SwapEffect: D3DSWAPEFFECT_DISCARD, - hDeviceWindow: WinHwnd(hwnd), - BackBufferFormat: D3DFMT_X8R8G8B8, - BackBufferWidth: 2, - BackBufferHeight: 2, - BackBufferCount: 1, - MultiSampleType: D3DMULTISAMPLE_NONE, - ..Default::default() - }; - let mut device: Option = None; - let base_flags = D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE; - let mut created = d3d9ex - .CreateDeviceEx( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_HARDWARE_VERTEXPROCESSING) as u32, - &mut params, - null_mut(), - &mut device, - ) - .is_ok(); - if !created { - created = d3d9ex - .CreateDeviceEx( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_SOFTWARE_VERTEXPROCESSING) as u32, - &mut params, - null_mut(), - &mut device, - ) - .is_ok(); - } - let Some(device) = (if created { device } else { None }) else { - let _ = DestroyWindow(hwnd); - verbose_log("d3d9 shared texture: D3D9Ex probe device creation failed"); - return None; - }; - - let raw = device.as_raw(); - let vtable = *(raw as *mut *const *const ()); - let check_resource_residency = *vtable.add(VT_DEVICE9EX_CHECK_RESOURCE_RESIDENCY) as *const u8; - let mut result = None; - const MAX_FUNC_SCAN_BYTES: usize = 200; - for offset in 0..MAX_FUNC_SCAN_BYTES { - let candidate = check_resource_residency.add(offset); - if !memory_range_readable(candidate, D3D9EX_PATTERN_LEN) { - break; - } - let bytes = std::slice::from_raw_parts(candidate, D3D9EX_PATTERN_LEN); - let Some((d3d9_object_field, is_d3d9ex_field)) = d3d9ex_pattern_offsets(bytes) else { - continue; - }; - let offsets = D3d9ExFlagOffsets { - d3d9_object_offset: read_u32_field(candidate, d3d9_object_field) as usize, - is_d3d9ex_offset: read_u32_field(candidate, is_d3d9ex_field) as usize, - }; - if validate_d3d9ex_offsets(raw, d3d9ex.as_raw(), offsets) { - verbose_log(&format!( - "d3d9 shared texture: discovered D3D9Ex flag offsets object=0x{:x} flag=0x{:x}", - offsets.d3d9_object_offset, offsets.is_d3d9ex_offset - )); - result = Some(offsets); - break; - } - } - - let _ = DestroyWindow(hwnd); - if result.is_none() { - verbose_log("d3d9 shared texture: D3D9Ex flag offset discovery failed"); - } - result -} - -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -unsafe fn d3d9ex_flag_offsets() -> Option { - *D3D9EX_FLAG_OFFSETS.get_or_init(|| discover_d3d9ex_flag_offsets()) -} - -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] -unsafe fn d3d9ex_flag_offsets() -> Option { - None -} - -unsafe fn d3d9ex_flag_for_device(device: &IDirect3DDevice9) -> Option<*mut i32> { - let offsets = d3d9ex_flag_offsets()?; - let device_raw = device.as_raw().cast::(); - let d3d9_object_slot = device_raw.add(offsets.d3d9_object_offset).cast::<*mut u8>(); - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - if !memory_range_readable(d3d9_object_slot.cast(), mem::size_of::<*mut u8>()) { - return None; - } - let d3d9_object = *d3d9_object_slot; - if d3d9_object.is_null() { - return None; - } - let flag = d3d9_object.add(offsets.is_d3d9ex_offset).cast::(); - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - if !memory_range_readable(flag.cast(), mem::size_of::()) { - return None; - } - Some(flag) -} - -unsafe fn force_d3d9ex_flag(device: &IDirect3DDevice9) -> Option { - let flag = d3d9ex_flag_for_device(device)?; - let previous = *flag; - *flag = 1; - Some(D3d9ExFlagGuard { flag, previous }) -} - -unsafe fn client_size_from_hwnd(hwnd: HWND) -> Option<(HWND, u32, u32)> { - if hwnd.is_null() || IsWindow(hwnd) == 0 { - return None; - } - let mut rect = mem::zeroed(); - if GetClientRect(hwnd, &mut rect) == 0 { - return None; - } - let width = (rect.right - rect.left).max(1) as u32; - let height = (rect.bottom - rect.top).max(1) as u32; - Some((hwnd, width, height)) -} - -unsafe fn create_d3d9_interop_d3d11_device() -> Option { - const FEATURE_LEVELS: &[windows::Win32::Graphics::Direct3D::D3D_FEATURE_LEVEL] = &[ - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - ]; - let factory = CreateDXGIFactory1::() - .map_err(|error| { - verbose_log(&format!( - "d3d9 shared texture: CreateDXGIFactory1 failed: {error}" - )); - error - }) - .ok()?; - let adapter = factory - .EnumAdapters(0) - .map_err(|error| { - verbose_log(&format!( - "d3d9 shared texture: EnumAdapters(0) failed: {error}" - )); - error - }) - .ok()?; - let mut d3d11_device = None; - if let Err(error) = D3D11CreateDevice( - &adapter, - D3D_DRIVER_TYPE_UNKNOWN, - WinHmodule::default(), - Default::default(), - Some(FEATURE_LEVELS), - D3D11_SDK_VERSION, - Some(&mut d3d11_device), - None, - None, - ) { - verbose_log(&format!( - "d3d9 shared texture: D3D11CreateDevice failed: {error}" - )); - return None; - } - d3d11_device -} - -unsafe fn create_d3d9_shared_texture_path( - device: &IDirect3DDevice9, - desc: &D3DSURFACE_DESC, -) -> Option { - let Some(dxgi_format) = d3d9_shared_dxgi_format(desc.Format) else { - verbose_log(&format!( - "d3d9 shared texture: unsupported D3D9 format {}", - desc.Format.0 - )); - return None; - }; - let d3d11_device = create_d3d9_interop_d3d11_device()?; - let texture_desc = D3D11_TEXTURE2D_DESC { - Width: desc.Width, - Height: desc.Height, - MipLevels: 1, - ArraySize: 1, - Format: dxgi_format, - 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 d3d11_texture = None; - if let Err(error) = d3d11_device.CreateTexture2D(&texture_desc, None, Some(&mut d3d11_texture)) - { - verbose_log(&format!( - "d3d9 shared texture: CreateTexture2D failed: {error}" - )); - return None; - } - let Some(d3d11_texture) = d3d11_texture else { - verbose_log("d3d9 shared texture: CreateTexture2D returned no texture"); - return None; - }; - let mut shared_handle = d3d11_texture - .cast::() - .map_err(|error| { - verbose_log(&format!( - "d3d9 shared texture: QueryInterface IDXGIResource failed: {error}" - )); - error - }) - .and_then(|resource| { - resource.GetSharedHandle().map_err(|error| { - verbose_log(&format!( - "d3d9 shared texture: GetSharedHandle failed: {error}" - )); - error - }) - }) - .ok()?; - if shared_handle.is_invalid() { - verbose_log("d3d9 shared texture: GetSharedHandle returned an invalid handle"); - return None; - } - let mut d3d9_texture = None; - let _d3d9ex_flag = force_d3d9ex_flag(device).or_else(|| { - verbose_log( - "d3d9 shared texture: D3D9Ex compatibility flag unavailable; trying direct CreateTexture", - ); - None - }); - if let Err(error) = device.CreateTexture( - desc.Width, - desc.Height, - 1, - D3DUSAGE_RENDERTARGET as u32, - desc.Format, - D3DPOOL_DEFAULT, - &mut d3d9_texture, - &mut shared_handle, - ) { - verbose_log(&format!( - "d3d9 shared texture: CreateTexture shared handle failed: {error}" - )); - return None; - } - let Some(d3d9_texture) = d3d9_texture else { - verbose_log("d3d9 shared texture: CreateTexture returned no D3D9 texture"); - return None; - }; - let copy_surface = d3d9_texture - .GetSurfaceLevel(0) - .map_err(|error| { - verbose_log(&format!( - "d3d9 shared texture: GetSurfaceLevel failed: {error}" - )); - error - }) - .ok()?; - Some(D3d9CapturePath::SharedTexture { - copy_surface, - _copy_texture: d3d9_texture, - _d3d11_device: d3d11_device, - _d3d11_texture: d3d11_texture, - shared_handle: shared_handle.0 as usize as u64, - dxgi_format, - }) -} - -unsafe fn create_d3d9_memory_path( - state: &mut HookState, - device: &IDirect3DDevice9, - desc: &D3DSURFACE_DESC, - format_mode: D3d11FormatMode, -) -> Option { - let mut surface: Option = None; - if device - .CreateOffscreenPlainSurface( - desc.Width, - desc.Height, - desc.Format, - D3DPOOL_SYSTEMMEM, - &mut surface, - null_mut(), - ) - .is_err() - { - (*state.info).last_error = 60; - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_DEVICE_LOST); - return None; - } - let Some(surface) = surface else { - (*state.info).last_error = 60; - return None; - }; - let mut resolve_surface = None; - if desc.MultiSampleType != D3DMULTISAMPLE_NONE { - if device - .CreateRenderTarget( - desc.Width, - desc.Height, - desc.Format, - D3DMULTISAMPLE_NONE, - 0, - false, - &mut resolve_surface, - null_mut(), - ) - .is_err() - || resolve_surface.is_none() - { - (*state.info).last_error = 61; - set_fallback_reason(state, crate::GAME_CAPTURE_FALLBACK_MULTISAMPLED); - set_capture_flags(state, crate::GAME_CAPTURE_FLAG_MULTISAMPLED); - return None; - } - } - Some(D3d9CapturePath::Memory { - surface, - resolve_surface, - format_mode, - }) -} - -unsafe fn d3d9_state_for_frame<'a>( - state: &'a mut HookState, - device: &IDirect3DDevice9, - device_ptr: usize, - desc: &D3DSURFACE_DESC, - format_mode: Option, - prefer_shared_texture: bool, -) -> Option<&'a mut D3d9CaptureState> { - let recreate = state - .d3d9 - .as_ref() - .map(|capture| { - capture.device_ptr != device_ptr - || capture.width != desc.Width - || capture.height != desc.Height - || capture.format != desc.Format - || capture.multisample_type != desc.MultiSampleType - }) - .unwrap_or(true); - if recreate { - let path = if prefer_shared_texture { - match create_d3d9_shared_texture_path(device, desc) { - Some(path) => { - verbose_log("d3d9 shared texture capture initialized"); - Some(path) - } - None => { - verbose_log("d3d9 shared texture init failed; falling back to memory readback"); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED); - None - } - } - } else { - None - } - .or_else(|| { - let format_mode = format_mode?; - create_d3d9_memory_path(state, device, desc, format_mode) - }); - let Some(path) = path else { - state.d3d9 = None; - return None; - }; - state.d3d9 = Some(D3d9CaptureState { - device_ptr, - width: desc.Width, - height: desc.Height, - format: desc.Format, - multisample_type: desc.MultiSampleType, - path, - }); - } - state.d3d9.as_mut() -} - -unsafe fn capture_d3d9_backbuffer( - device: &IDirect3DDevice9, - back_buffer: IDirect3DSurface9, - window_override: WinHwnd, -) { - if !HOOKS_READY.load(Ordering::Acquire) { - return; - } - let should_continue = { - let Some(state) = ensure_state() else { - return; - }; - capture_should_run(state) - }; - if !should_continue { - clear_state(); - return; - } - let Some(state) = ensure_state() else { - return; - }; - - if !D3D9_FIRED.swap(true, Ordering::AcqRel) { - verbose_log("d3d9 present detour fired (first frame, valid device)"); - } - mark_present(state, GAME_CAPTURE_API_D3D9); - set_capture_flags(state, 0); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_NONE); - - let mut desc = D3DSURFACE_DESC::default(); - if back_buffer.GetDesc(&mut desc).is_err() || desc.Width == 0 || desc.Height == 0 { - record_dropped_frame(state); - return; - } - - let format_mode = d3d9_format_mode(desc.Format); - if format_mode.is_none() && d3d9_shared_dxgi_format(desc.Format).is_none() { - set_capture_flags(state, d3d9_hdr_flags(desc.Format)); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED); - record_dropped_frame(state); - return; - }; - - if !frame_due(state) { - record_dropped_frame(state); - return; - } - - if desc.MultiSampleType != D3DMULTISAMPLE_NONE { - set_capture_flags(state, crate::GAME_CAPTURE_FLAG_MULTISAMPLED); - } - set_capture_flags( - state, - (*state.info).capture_flags | d3d9_hdr_flags(desc.Format), - ); - - let device_ptr = device.as_raw() as usize; - let force_cpu = env_flag_enabled(ENV_FORCE_CPU); - if force_cpu { - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORCED_CPU); - } - - let Some(capture_state) = - d3d9_state_for_frame(state, device, device_ptr, &desc, format_mode, !force_cpu) - else { - record_dropped_frame(state); - return; - }; - - if let D3d9CapturePath::SharedTexture { - copy_surface, - shared_handle, - dxgi_format, - .. - } = &capture_state.path - { - let copy_surface = copy_surface.clone(); - let shared_handle = *shared_handle; - let dxgi_format = *dxgi_format; - if device - .StretchRect( - &back_buffer, - null_mut(), - ©_surface, - null_mut(), - D3DTEXF_NONE, - ) - .is_ok() - { - let hwnd = desc_window(state, window_override); - let _ = publish_shared_texture_frame( - state, - hwnd, - desc.Width, - desc.Height, - dxgi_format, - shared_handle, - ); - return; - } - verbose_log("d3d9 shared texture StretchRect failed; falling back to memory readback"); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED); - state.d3d9 = None; - } - - let (surface, source_surface, format_mode) = { - let Some(capture_state) = - d3d9_state_for_frame(state, device, device_ptr, &desc, format_mode, false) - else { - record_dropped_frame(state); - return; - }; - let D3d9CapturePath::Memory { - surface, - resolve_surface, - format_mode, - } = &capture_state.path - else { - record_dropped_frame(state); - return; - }; - let source_surface = if desc.MultiSampleType != D3DMULTISAMPLE_NONE { - let Some(resolve_surface) = resolve_surface.clone() else { - record_dropped_frame(state); - return; - }; - if device - .StretchRect( - &back_buffer, - null_mut(), - &resolve_surface, - null_mut(), - D3DTEXF_NONE, - ) - .is_err() - { - verbose_log("d3d9 StretchRect resolve FAILED (multisample backbuffer)"); - set_fallback_reason(state, crate::GAME_CAPTURE_FALLBACK_MULTISAMPLED); - record_dropped_frame(state); - return; - } - resolve_surface - } else { - back_buffer.clone() - }; - (surface.clone(), source_surface, *format_mode) - }; - - if device - .GetRenderTargetData(&source_surface, &surface) - .is_err() - { - verbose_log("d3d9 GetRenderTargetData FAILED (readback surface or lost device)"); - if desc.MultiSampleType != D3DMULTISAMPLE_NONE { - set_fallback_reason(state, crate::GAME_CAPTURE_FALLBACK_MULTISAMPLED); - set_capture_flags(state, crate::GAME_CAPTURE_FLAG_MULTISAMPLED); - } else { - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_DEVICE_LOST); - state.d3d9 = None; - } - record_dropped_frame(state); - return; - } - - let mut locked = D3DLOCKED_RECT::default(); - if surface - .LockRect(&mut locked, null_mut(), D3DLOCK_READONLY as u32) - .is_err() - || locked.pBits.is_null() - { - state.d3d9 = None; - record_dropped_frame(state); - return; - } - - let hwnd = desc_window(state, window_override); - let _ = write_bgra_rows_to_shared_memory( - state, - hwnd, - desc.Width, - desc.Height, - locked.pBits as *const u8, - locked.Pitch.max(0) as usize, - false, - format_mode, - ); - let _ = surface.UnlockRect(); -} - -unsafe fn capture_d3d9_device_frame(device_raw: *mut c_void, window_override: WinHwnd) { - let Some(device) = IDirect3DDevice9::from_raw_borrowed(&device_raw) else { - return; - }; - let Ok(back_buffer) = device.GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO) else { - if let Some(state) = ensure_state() { - state.d3d9 = None; - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_DEVICE_LOST); - record_dropped_frame(state); - } - return; - }; - capture_d3d9_backbuffer(device, back_buffer, window_override); -} - -unsafe fn capture_d3d9_swapchain_frame(swap_chain_raw: *mut c_void, window_override: WinHwnd) { - let Some(swap_chain) = IDirect3DSwapChain9::from_raw_borrowed(&swap_chain_raw) else { - return; - }; - let Ok(device) = swap_chain.GetDevice() else { - return; - }; - let Ok(back_buffer) = swap_chain.GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO) else { - if let Some(state) = ensure_state() { - state.d3d9 = None; - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_DEVICE_LOST); - record_dropped_frame(state); - } - return; - }; - capture_d3d9_backbuffer(&device, back_buffer, window_override); -} - -unsafe fn present_enter() -> bool { - D3D9_PRESENT_RECURSE.fetch_add(1, Ordering::AcqRel) == 0 -} - -unsafe fn present_leave() { - D3D9_PRESENT_RECURSE.fetch_sub(1, Ordering::AcqRel); -} - -unsafe fn desc_window(state: &mut HookState, window_override: WinHwnd) -> HWND { - let override_hwnd = window_override.0 as HWND; - if let Some((hwnd, _, _)) = client_size_from_hwnd(override_hwnd) { - return hwnd; - } - let info_hwnd = (*state.info).hwnd as usize as HWND; - if let Some((hwnd, _, _)) = client_size_from_hwnd(info_hwnd) { - return hwnd; - } - info_hwnd -} - -unsafe extern "system" fn d3d9_present_detour( - device: *mut c_void, - source_rect: *const RECT, - dest_rect: *const RECT, - dest_window_override: WinHwnd, - dirty_region: *const RGNDATA, -) -> windows::core::HRESULT { - let outermost = present_enter(); - if outermost { - capture_d3d9_device_frame(device, dest_window_override); - } - if let Some(hook) = D3D9_PRESENT_HOOK.as_ref() { - let result = (hook.trampoline_fn())( - device, - source_rect, - dest_rect, - dest_window_override, - dirty_region, - ); - present_leave(); - return result; - } - present_leave(); - windows::core::HRESULT(0) -} - -unsafe extern "system" fn d3d9_present_ex_detour( - device: *mut c_void, - source_rect: *const RECT, - dest_rect: *const RECT, - dest_window_override: WinHwnd, - dirty_region: *const RGNDATA, - flags: u32, -) -> windows::core::HRESULT { - let outermost = present_enter(); - if outermost { - capture_d3d9_device_frame(device, dest_window_override); - } - if let Some(hook) = D3D9_PRESENT_EX_HOOK.as_ref() { - let result = (hook.trampoline_fn())( - device, - source_rect, - dest_rect, - dest_window_override, - dirty_region, - flags, - ); - present_leave(); - return result; - } - present_leave(); - windows::core::HRESULT(0) -} - -unsafe extern "system" fn d3d9_reset_detour( - device: *mut c_void, - present_parameters: *mut D3DPRESENT_PARAMETERS, -) -> windows::core::HRESULT { - if let Some(state) = ensure_state() { - state.d3d9 = None; - verbose_log("d3d9 Reset: invalidated cached readback surface"); - } - if let Some(hook) = D3D9_RESET_HOOK.as_ref() { - return (hook.trampoline_fn())(device, present_parameters); - } - windows::core::HRESULT(0) -} - -unsafe extern "system" fn d3d9_reset_ex_detour( - device: *mut c_void, - present_parameters: *mut D3DPRESENT_PARAMETERS, - fullscreen_display_mode: *mut D3DDISPLAYMODEEX, -) -> windows::core::HRESULT { - if let Some(state) = ensure_state() { - state.d3d9 = None; - verbose_log("d3d9 ResetEx: invalidated cached readback surface"); - } - if let Some(hook) = D3D9_RESET_EX_HOOK.as_ref() { - return (hook.trampoline_fn())(device, present_parameters, fullscreen_display_mode); - } - windows::core::HRESULT(0) -} - -unsafe extern "system" fn d3d9_swapchain_present_detour( - swap_chain: *mut c_void, - source_rect: *const RECT, - dest_rect: *const RECT, - dest_window_override: WinHwnd, - dirty_region: *const RGNDATA, - flags: u32, -) -> windows::core::HRESULT { - let outermost = present_enter(); - if outermost { - capture_d3d9_swapchain_frame(swap_chain, dest_window_override); - } - if let Some(hook) = D3D9_SWAPCHAIN_PRESENT_HOOK.as_ref() { - let result = (hook.trampoline_fn())( - swap_chain, - source_rect, - dest_rect, - dest_window_override, - dirty_region, - flags, - ); - present_leave(); - return result; - } - present_leave(); - windows::core::HRESULT(0) -} - -unsafe fn create_dummy_window_d3d9() -> HWND { - let class_name = wide("FluxerGameCaptureDummyWindow"); - CreateWindowExW( - 0, - class_name.as_ptr(), - class_name.as_ptr(), - WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, - CW_USEDEFAULT, - 2, - 2, - null_mut(), - null_mut(), - null_mut(), - null_mut(), - ) -} - -unsafe fn d3d9_method_pointers() -> Option<( - *const (), - *const (), - Option<*const ()>, - Option<*const ()>, - Option<*const ()>, -)> { - let d3d9 = Direct3DCreate9(D3D_SDK_VERSION)?; - let hwnd = create_dummy_window_d3d9(); - if hwnd.is_null() { - return None; - } - let mut params = D3DPRESENT_PARAMETERS { - Windowed: windows::core::BOOL(1), - SwapEffect: D3DSWAPEFFECT_DISCARD, - hDeviceWindow: WinHwnd(hwnd), - BackBufferFormat: D3DFMT_X8R8G8B8, - BackBufferWidth: 2, - BackBufferHeight: 2, - BackBufferCount: 1, - MultiSampleType: D3DMULTISAMPLE_NONE, - ..Default::default() - }; - - let mut device: Option = None; - let base_flags = D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE; - let mut created = d3d9 - .CreateDevice( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_HARDWARE_VERTEXPROCESSING) as u32, - &mut params, - &mut device, - ) - .is_ok(); - if !created { - created = d3d9 - .CreateDevice( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_SOFTWARE_VERTEXPROCESSING) as u32, - &mut params, - &mut device, - ) - .is_ok(); - } - let device = if created { device } else { None }; - let Some(device) = device else { - let _ = DestroyWindow(hwnd); - return None; - }; - - let raw = device.as_raw(); - let vtable = *(raw as *mut *const *const ()); - let reset = *vtable.add(VT_DEVICE9_RESET); - let present = *vtable.add(VT_DEVICE9_PRESENT); - - let swapchain_present = device.GetSwapChain(0).ok().map(|swap_chain| { - let raw = swap_chain.as_raw(); - let vtable = *(raw as *mut *const *const ()); - *vtable.add(VT_SWAPCHAIN9_PRESENT) - }); - - let (present_ex, reset_ex) = d3d9ex_method_pointers(); - - drop(device); - let _ = DestroyWindow(hwnd); - Some((present, reset, present_ex, reset_ex, swapchain_present)) -} - -unsafe fn d3d9ex_method_pointers() -> (Option<*const ()>, Option<*const ()>) { - use windows::Win32::Graphics::Direct3D9::{Direct3DCreate9Ex, IDirect3DDevice9Ex}; - let Ok(d3d9ex) = Direct3DCreate9Ex(D3D_SDK_VERSION) else { - return (None, None); - }; - let hwnd = create_dummy_window_d3d9(); - if hwnd.is_null() { - return (None, None); - } - let mut params = D3DPRESENT_PARAMETERS { - Windowed: windows::core::BOOL(1), - SwapEffect: D3DSWAPEFFECT_DISCARD, - hDeviceWindow: WinHwnd(hwnd), - BackBufferFormat: D3DFMT_X8R8G8B8, - BackBufferWidth: 2, - BackBufferHeight: 2, - BackBufferCount: 1, - MultiSampleType: D3DMULTISAMPLE_NONE, - ..Default::default() - }; - let mut device: Option = None; - let base_flags = D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE; - let mut created = d3d9ex - .CreateDeviceEx( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_HARDWARE_VERTEXPROCESSING) as u32, - &mut params, - null_mut(), - &mut device, - ) - .is_ok(); - if !created { - created = d3d9ex - .CreateDeviceEx( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - WinHwnd(hwnd), - (base_flags | D3DCREATE_SOFTWARE_VERTEXPROCESSING) as u32, - &mut params, - null_mut(), - &mut device, - ) - .is_ok(); - } - let device = if created { device } else { None }; - let result = device.map(|device| { - let raw = device.as_raw(); - let vtable = *(raw as *mut *const *const ()); - ( - *vtable.add(VT_DEVICE9EX_PRESENTEX), - *vtable.add(VT_DEVICE9EX_RESETEX), - ) - }); - let _ = DestroyWindow(hwnd); - result - .map(|(present_ex, reset_ex)| (Some(present_ex), Some(reset_ex))) - .unwrap_or((None, None)) -} - -pub(crate) unsafe fn install_d3d9_hooks() { - if D3D9_PRESENT_HOOK.is_some() { - return; - } - let Some((present, reset, present_ex, reset_ex, swapchain_present)) = d3d9_method_pointers() - else { - verbose_log("d3d9 install: method_pointers FAILED (throwaway D3D9 device/vtable probe)"); - return; - }; - verbose_log("d3d9 install: method_pointers resolved, enabling detours"); - - let target: D3d9PresentFn = mem::transmute(present); - let detour: D3d9PresentFn = d3d9_present_detour; - if let Ok(hook) = Detour::::new(target, detour) { - if hook.enable().is_ok() { - D3D9_PRESENT_HOOK = Some(hook); - verbose_log("d3d9 Present hooked"); - } - } - - if D3D9_RESET_HOOK.is_none() { - let target: D3d9ResetFn = mem::transmute(reset); - let detour: D3d9ResetFn = d3d9_reset_detour; - if let Ok(hook) = Detour::::new(target, detour) { - if hook.enable().is_ok() { - D3D9_RESET_HOOK = Some(hook); - } - } - } - - if let Some(reset_ex) = reset_ex { - if D3D9_RESET_EX_HOOK.is_none() { - let target: D3d9ResetExFn = mem::transmute(reset_ex); - let detour: D3d9ResetExFn = d3d9_reset_ex_detour; - if let Ok(hook) = Detour::::new(target, detour) { - if hook.enable().is_ok() { - D3D9_RESET_EX_HOOK = Some(hook); - verbose_log("d3d9 ResetEx hooked"); - } - } - } - } - - if let Some(present_ex) = present_ex { - if D3D9_PRESENT_EX_HOOK.is_none() { - let target: D3d9PresentExFn = mem::transmute(present_ex); - let detour: D3d9PresentExFn = d3d9_present_ex_detour; - if let Ok(hook) = Detour::::new(target, detour) { - if hook.enable().is_ok() { - D3D9_PRESENT_EX_HOOK = Some(hook); - verbose_log("d3d9 PresentEx hooked"); - } - } - } - } - - if let Some(swapchain_present) = swapchain_present { - if D3D9_SWAPCHAIN_PRESENT_HOOK.is_none() { - let target: D3d9SwapChainPresentFn = mem::transmute(swapchain_present); - let detour: D3d9SwapChainPresentFn = d3d9_swapchain_present_detour; - if let Ok(hook) = Detour::::new(target, detour) { - if hook.enable().is_ok() { - D3D9_SWAPCHAIN_PRESENT_HOOK = Some(hook); - verbose_log("d3d9 SwapChain Present hooked"); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(target_arch = "x86_64")] - #[test] - fn d3d9ex_pattern_matches_win11_x64_signature() { - let bytes = [ - 0x49, 0x8B, 0x86, 0x30, 0x40, 0x00, 0x00, 0x83, 0xB8, 0xA0, 0x55, 0x00, 0x00, 0x00, - 0x75, 0x12, 0x48, 0x8D, 0x15, 0xB9, 0x24, 0x0A, - ]; - assert_eq!(d3d9ex_pattern_offsets(&bytes), Some((3, 9))); - unsafe { - assert_eq!(read_u32_field(bytes.as_ptr(), 3), 0x4030); - assert_eq!(read_u32_field(bytes.as_ptr(), 9), 0x55A0); - } - } - - #[cfg(target_arch = "x86")] - #[test] - fn d3d9ex_pattern_matches_win11_x86_signature() { - let bytes = [ - 0x8B, 0x83, 0x3C, 0x2B, 0x00, 0x00, 0x39, 0xB8, 0x44, 0x4F, 0x00, 0x00, 0x75, 0x0F, - 0xBA, 0xD0, 0xC6, 0x00, 0x10, 0x90, - ]; - assert_eq!(d3d9ex_pattern_offsets(&bytes), Some((2, 8))); - unsafe { - assert_eq!(read_u32_field(bytes.as_ptr(), 2), 0x2B3C); - assert_eq!(read_u32_field(bytes.as_ptr(), 8), 0x4F44); - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/hook/src/gl_interop.rs b/fluxer_desktop/native/win-game-capture/hook/src/gl_interop.rs deleted file mode 100644 index c691ce7e1..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/src/gl_interop.rs +++ /dev/null @@ -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(name: &[u8]) -> Option { - 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 { - if wglGetCurrentContext().is_null() { - return None; - } - let open_device = load_proc::(b"wglDXOpenDeviceNV\0")?; - let close_device = load_proc::(b"wglDXCloseDeviceNV\0")?; - let register_object = load_proc::(b"wglDXRegisterObjectNV\0")?; - let unregister_object = load_proc::(b"wglDXUnregisterObjectNV\0")?; - let lock_objects = load_proc::(b"wglDXLockObjectsNV\0")?; - let unlock_objects = load_proc::(b"wglDXUnlockObjectsNV\0")?; - let gen_framebuffers = load_proc::(b"glGenFramebuffers\0")?; - let delete_framebuffers = load_proc::(b"glDeleteFramebuffers\0")?; - let bind_framebuffer = load_proc::(b"glBindFramebuffer\0")?; - let framebuffer_texture_2d = - load_proc::(b"glFramebufferTexture2D\0")?; - let check_framebuffer_status = - load_proc::(b"glCheckFramebufferStatus\0")?; - let blit_framebuffer = load_proc::(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::() - .and_then(|resource| resource.GetSharedHandle()) - .ok()?; - Some((texture, handle.0 as usize as u64)) -} - -impl GlInteropState { - unsafe fn create(width: u32, height: u32) -> Option { - 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 -} diff --git a/fluxer_desktop/native/win-game-capture/hook/src/inline_hook.rs b/fluxer_desktop/native/win-game-capture/hook/src/inline_hook.rs deleted file mode 100644 index 20ce31568..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/src/inline_hook.rs +++ /dev/null @@ -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 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 { - inner: Inner, -} - -#[cfg(not(target_arch = "aarch64"))] -enum Inner { - Retour(retour::GenericDetour), -} - -#[cfg(target_arch = "aarch64")] -enum Inner { - Aarch64(aarch64::Aarch64Detour), -} - -impl Detour { - pub(crate) unsafe fn new(target: T, detour: T) -> Result { - #[cfg(not(target_arch = "aarch64"))] - { - match retour::GenericDetour::::new(target, detour) { - Ok(detour) => Ok(Self { - inner: Inner::Retour(detour), - }), - Err(_) => Err(()), - } - } - #[cfg(target_arch = "aarch64")] - { - aarch64::Aarch64Detour::::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 { - target: *mut u8, - detour: *const u8, - trampoline: *mut u8, - original_prologue: [u8; STOLEN_BYTES], - enabled: std::cell::Cell, - _marker: PhantomData, - } - - unsafe impl Send for Aarch64Detour {} - unsafe impl Sync for Aarch64Detour {} - - impl Aarch64Detour { - pub(super) unsafe fn new(target: T, detour: T) -> Result { - 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 Drop for Aarch64Detour { - fn drop(&mut self) { - unsafe { - self.disable(); - if !self.trampoline.is_null() { - VirtualFree(self.trampoline.cast(), 0, MEM_RELEASE); - self.trampoline = ptr::null_mut(); - } - } - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/hook/src/lib.rs b/fluxer_desktop/native/win-game-capture/hook/src/lib.rs deleted file mode 100644 index 1610dc534..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/src/lib.rs +++ /dev/null @@ -1,2168 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![cfg(target_os = "windows")] -#![allow(dead_code)] -#![allow(static_mut_refs)] -#![allow(unsafe_op_in_unsafe_fn)] - -#[path = "../../src/game_capture_abi.rs"] -mod game_capture_abi; - -mod arm64_reloc; -mod d3d9; -mod gl_interop; -mod inline_hook; - -pub(crate) use game_capture_abi::{ - ENV_DISABLE_D3D12, ENV_DISABLE_OPENGL_SHARED_TEXTURE, ENV_ENABLE_OPENGL_SHARED_TEXTURE, - ENV_FORCE_CPU, ENV_FORCE_SHARED_TEXTURE, ENV_VERBOSE, GAME_CAPTURE_API_D3D9, - GAME_CAPTURE_API_D3D10, GAME_CAPTURE_API_D3D11, GAME_CAPTURE_API_D3D12, - GAME_CAPTURE_API_OPENGL, GAME_CAPTURE_BUFFER_COUNT, - GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE, GAME_CAPTURE_FALLBACK_DEVICE_LOST, - GAME_CAPTURE_FALLBACK_FORCED_CPU, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED, - GAME_CAPTURE_FALLBACK_MULTISAMPLED, GAME_CAPTURE_FALLBACK_NONE, - GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED, GAME_CAPTURE_FLAG_FLIP_VERTICAL, - GAME_CAPTURE_FLAG_HDR, GAME_CAPTURE_FLAG_MULTISAMPLED, GAME_CAPTURE_FLAG_TEN_BIT, - GAME_CAPTURE_FRAME_PREFIX, GAME_CAPTURE_INFO_PREFIX, GAME_CAPTURE_KEEPALIVE_PREFIX, - GAME_CAPTURE_MAGIC, GAME_CAPTURE_PRESENT_CLOCK_QPC, GAME_CAPTURE_READY_PREFIX, - GAME_CAPTURE_STATE_ACTIVE, GAME_CAPTURE_STATE_ERROR, GAME_CAPTURE_STATE_RESIZE_REQUIRED, - GAME_CAPTURE_STATE_STOPPED, GAME_CAPTURE_STOP_PREFIX, GAME_CAPTURE_TRANSPORT_MEMORY, - GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE, GameCaptureSharedInfo, env_flag_enabled, - frame_buffer_size, host_supports_present_clock, mutex_name, object_name, qpc_now_us, -}; -pub(crate) use inline_hook::Detour; -use std::{ - ffi::c_void, - mem, - ptr::{null, null_mut}, - sync::{ - Mutex, OnceLock, - atomic::{AtomicBool, AtomicU32, Ordering}, - }, -}; -use windows::{ - Win32::{ - Foundation::{HMODULE as WinHmodule, HWND as WinHwnd}, - Graphics::{ - Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}, - Direct3D10::{ - D3D10_BIND_RENDER_TARGET, D3D10_BIND_SHADER_RESOURCE, D3D10_RESOURCE_MISC_SHARED, - D3D10_TEXTURE2D_DESC, D3D10_USAGE_DEFAULT, ID3D10Device, ID3D10Texture2D, - }, - Direct3D11::{ - D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_READ, - D3D11_CREATE_DEVICE_FLAG, D3D11_MAP_READ, D3D11_RESOURCE_MISC_SHARED, - D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, - D3D11CreateDeviceAndSwapChain, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, - ID3D11Texture2D, - }, - Direct3D11on12::{D3D11_RESOURCE_FLAGS, D3D11On12CreateDevice, ID3D11On12Device}, - Direct3D12::{ - D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_QUEUE_DESC, - D3D12_RESOURCE_STATE_PRESENT, ID3D12CommandQueue, ID3D12Device, ID3D12Resource, - }, - Dxgi::{ - Common::{ - DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, - DXGI_FORMAT_B8G8R8X8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, - DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, - DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, - DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16B16A16_UNORM, DXGI_MODE_DESC, - DXGI_MODE_SCALING_UNSPECIFIED, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, - DXGI_RATIONAL, DXGI_SAMPLE_DESC, - }, - DXGI_PRESENT_PARAMETERS, DXGI_SWAP_CHAIN_DESC, DXGI_SWAP_EFFECT_DISCARD, - DXGI_USAGE_RENDER_TARGET_OUTPUT, IDXGIResource, IDXGISwapChain, IDXGISwapChain1, - IDXGISwapChain3, - }, - }, - }, - core::{BOOL as WinBool, GUID, HRESULT, IUnknown, Interface}, -}; -use windows_sys::Win32::Foundation::{LPARAM as WinLparam, LRESULT, WPARAM}; -use windows_sys::{ - Win32::{ - Foundation::{CloseHandle, HANDLE, HINSTANCE, HWND, WAIT_ABANDONED, WAIT_OBJECT_0}, - Graphics::{ - Gdi::{HDC, WindowFromDC}, - OpenGL::{ - GL_BACK, GL_BGRA_EXT, GL_PACK_ALIGNMENT, GL_PACK_LSB_FIRST, GL_PACK_ROW_LENGTH, - GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS, GL_PACK_SWAP_BYTES, GL_READ_BUFFER, - GL_UNSIGNED_BYTE, glGetIntegerv, glPixelStorei, glReadBuffer, glReadPixels, - wglGetProcAddress, - }, - }, - System::{ - LibraryLoader::{DisableThreadLibraryCalls, GetModuleHandleW, GetProcAddress}, - Memory::{ - FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW, - UnmapViewOfFile, - }, - SystemServices::DLL_PROCESS_ATTACH, - Threading::{ - CreateThread, EVENT_ALL_ACCESS, MUTEX_ALL_ACCESS, OpenEventW, OpenMutexW, - ReleaseMutex, SYNCHRONIZATION_SYNCHRONIZE, SetEvent, Sleep, WaitForSingleObject, - }, - }, - UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CallNextHookEx, CreateWindowExW, DefWindowProcW, - DestroyWindow, GetClientRect, IsWindow, RegisterClassW, WNDCLASSW, WS_OVERLAPPEDWINDOW, - }, - }, - core::BOOL, -}; - -type SwapBuffersFn = unsafe extern "system" fn(HDC) -> BOOL; -type WglSwapLayerBuffersFn = unsafe extern "system" fn(HDC, u32) -> BOOL; -type GlBindBufferFn = unsafe extern "system" fn(u32, u32); -type DxgiPresentFn = unsafe extern "system" fn(*mut c_void, u32, u32) -> HRESULT; -type DxgiPresent1Fn = - unsafe extern "system" fn(*mut c_void, u32, u32, *const DXGI_PRESENT_PARAMETERS) -> HRESULT; -type D3d12ExecuteCommandListsFn = unsafe extern "system" fn(*mut c_void, u32, *const *mut c_void); -type D3d12CreateDeviceFn = unsafe extern "system" fn( - *mut c_void, - windows::Win32::Graphics::Direct3D::D3D_FEATURE_LEVEL, - *const GUID, - *mut *mut c_void, -) -> HRESULT; - -static mut SWAP_BUFFERS_HOOK: Option> = None; -static mut WGL_SWAP_LAYER_BUFFERS_HOOK: Option> = None; -static mut DXGI_PRESENT_HOOK: Option> = None; -static mut DXGI_PRESENT1_HOOK: Option> = None; -static mut D3D12_EXECUTE_COMMAND_LISTS_HOOK: Option> = None; -pub(crate) static HOOKS_READY: AtomicBool = AtomicBool::new(false); -static DXGI_PRESENT_DEPTH: AtomicU32 = AtomicU32::new(0); -static DXGI_PRESENT_ATTEMPTED: AtomicBool = AtomicBool::new(false); -static DXGI_PRESENT_FIRED: AtomicBool = AtomicBool::new(false); -static DXGI_PRESENT1_FIRED: AtomicBool = AtomicBool::new(false); -static OPENGL_PRESENT_FIRED: AtomicBool = AtomicBool::new(false); -static D3D12_EXECUTE_FIRED: AtomicBool = AtomicBool::new(false); - -const GL_PIXEL_PACK_BUFFER: u32 = 0x88EB; -const GL_PIXEL_PACK_BUFFER_BINDING: u32 = 0x88ED; - -const MAX_D3D12_QUEUE_CANDIDATES: usize = 8; -static D3D12_QUEUE_CANDIDATES: Mutex> = Mutex::new(Vec::new()); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum D3d11FormatMode { - Bgra, - Rgba, -} - -struct D3d11CaptureState { - device_ptr: usize, - context: ID3D11DeviceContext, - texture: ID3D11Texture2D, - staging: Option, - shared_handle: u64, - width: u32, - height: u32, - format: DXGI_FORMAT, - format_mode: D3d11FormatMode, - sample_count: u32, - force_cpu: bool, -} - -struct D3d12CaptureState { - device_ptr: usize, - device11: ID3D11Device, - context11: ID3D11DeviceContext, - device11on12: ID3D11On12Device, - copy_tex: ID3D11Texture2D, - staging: Option, - shared_handle: u64, - width: u32, - height: u32, - format: DXGI_FORMAT, - format_mode: D3d11FormatMode, - backbuffer_count: u32, - current_backbuffer: u32, - dxgi14: bool, - force_cpu: bool, -} - -struct D3d10CaptureState { - device_ptr: usize, - device: ID3D10Device, - texture: ID3D10Texture2D, - staging: Option, - shared_handle: u64, - width: u32, - height: u32, - format: DXGI_FORMAT, - format_mode: D3d11FormatMode, - sample_count: u32, - force_cpu: bool, -} - -pub(crate) struct HookState { - info_map: HANDLE, - frame_map: HANDLE, - ready_event: HANDLE, - stop_event: HANDLE, - mutexes: [HANDLE; GAME_CAPTURE_BUFFER_COUNT], - pub(crate) info: *mut GameCaptureSharedInfo, - frame_base: *mut u8, - frame_buffer_capacity: usize, - next_frame_index: usize, - scratch: Vec, - last_frame_ns: u64, - keepalive_name: Vec, - d3d11: Option, - d3d12: Option, - d3d10: Option, - pub(crate) d3d9: Option, - pub(crate) gl_interop: Option, -} - -unsafe impl Send for HookState {} - -static mut STATE: Option = None; - -pub(crate) fn wide(value: &str) -> Vec { - value.encode_utf16().chain(std::iter::once(0)).collect() -} - -fn current_pid() -> u32 { - std::process::id() -} - -unsafe fn close_handle(handle: &mut HANDLE) { - if !handle.is_null() { - CloseHandle(*handle); - *handle = null_mut(); - } -} - -unsafe fn open_event(prefix: &str, pid: u32) -> HANDLE { - let name = wide(&object_name(prefix, pid)); - OpenEventW(EVENT_ALL_ACCESS, 0, name.as_ptr()) -} - -unsafe fn open_mutex(name: &str) -> HANDLE { - let name = wide(name); - OpenMutexW(MUTEX_ALL_ACCESS, 0, name.as_ptr()) -} - -unsafe fn parent_alive(keepalive_name: &[u16]) -> bool { - let handle = OpenMutexW(SYNCHRONIZATION_SYNCHRONIZE, 0, keepalive_name.as_ptr()); - if handle.is_null() { - return false; - } - CloseHandle(handle); - true -} - -unsafe fn map_view(handle: HANDLE, size: usize) -> *mut T { - let view = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, size); - view.Value.cast() -} - -unsafe fn open_ipc() -> Option { - let pid = current_pid(); - let info_name = wide(&object_name(GAME_CAPTURE_INFO_PREFIX, pid)); - let frame_name = wide(&object_name(GAME_CAPTURE_FRAME_PREFIX, pid)); - let info_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, info_name.as_ptr()); - if info_map.is_null() { - return None; - } - let frame_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, frame_name.as_ptr()); - if frame_map.is_null() { - let mut handle = info_map; - close_handle(&mut handle); - return None; - } - let info = map_view::(info_map, mem::size_of::()); - if info.is_null() || (*info).magic != GAME_CAPTURE_MAGIC { - let mut info_map = info_map; - let mut frame_map = frame_map; - close_handle(&mut frame_map); - close_handle(&mut info_map); - return None; - } - let max_width = (*info).max_width; - let max_height = (*info).max_height; - let frame_buffer_capacity = frame_buffer_size(max_width, max_height).unwrap_or(0); - if frame_buffer_capacity == 0 { - let mut info_map = info_map; - let mut frame_map = frame_map; - close_handle(&mut frame_map); - close_handle(&mut info_map); - return None; - } - let frame_map_size = frame_buffer_capacity * GAME_CAPTURE_BUFFER_COUNT; - let frame_base = map_view::(frame_map, frame_map_size); - if frame_base.is_null() { - let mut info_map = info_map; - let mut frame_map = frame_map; - close_handle(&mut frame_map); - close_handle(&mut info_map); - return None; - } - - let ready_event = open_event(GAME_CAPTURE_READY_PREFIX, pid); - let stop_event = open_event(GAME_CAPTURE_STOP_PREFIX, pid); - let mutex_names = [mutex_name(pid, 0), mutex_name(pid, 1)]; - let mutexes = [open_mutex(&mutex_names[0]), open_mutex(&mutex_names[1])]; - if ready_event.is_null() - || stop_event.is_null() - || mutexes.iter().any(|handle| handle.is_null()) - { - let mut state = HookState { - info_map, - frame_map, - ready_event, - stop_event, - mutexes, - info, - frame_base, - frame_buffer_capacity, - next_frame_index: 0, - scratch: Vec::new(), - last_frame_ns: 0, - keepalive_name: wide(&object_name(GAME_CAPTURE_KEEPALIVE_PREFIX, pid)), - d3d11: None, - d3d12: None, - d3d10: None, - d3d9: None, - gl_interop: None, - }; - free_state(&mut state); - return None; - } - - Some(HookState { - info_map, - frame_map, - ready_event, - stop_event, - mutexes, - info, - frame_base, - frame_buffer_capacity, - next_frame_index: 0, - scratch: Vec::new(), - last_frame_ns: 0, - keepalive_name: wide(&object_name(GAME_CAPTURE_KEEPALIVE_PREFIX, pid)), - d3d11: None, - d3d12: None, - d3d10: None, - d3d9: None, - gl_interop: None, - }) -} - -unsafe fn free_state(state: &mut HookState) { - if !state.info.is_null() { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: state.info.cast(), - }); - state.info = null_mut(); - } - if !state.frame_base.is_null() { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: state.frame_base.cast(), - }); - state.frame_base = null_mut(); - } - close_handle(&mut state.mutexes[0]); - close_handle(&mut state.mutexes[1]); - close_handle(&mut state.stop_event); - close_handle(&mut state.ready_event); - close_handle(&mut state.frame_map); - close_handle(&mut state.info_map); -} - -pub(crate) unsafe fn ensure_state() -> Option<&'static mut HookState> { - if STATE.is_none() { - STATE = open_ipc(); - } - STATE.as_mut() -} - -pub(crate) unsafe fn clear_state() { - if let Some(mut state) = STATE.take() { - free_state(&mut state); - } - reset_d3d12_queue_candidates(); -} - -pub(crate) fn now_ns() -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos() as u64) - .unwrap_or(0) -} - -pub(crate) fn now_us() -> i64 { - (now_ns() / 1_000) as i64 -} - -pub(crate) unsafe fn mark_present(state: &mut HookState, api: u32) { - let info = &mut *state.info; - info.api_type = api; - if host_supports_present_clock(info.version) { - info.present_clock = GAME_CAPTURE_PRESENT_CLOCK_QPC; - info.last_present_timestamp_us = qpc_now_us(); - } else { - info.last_present_timestamp_us = now_us(); - } -} - -pub(crate) unsafe fn record_dropped_frame(state: &mut HookState) { - let info = &mut *state.info; - info.dropped_frame_counter = info.dropped_frame_counter.wrapping_add(1); -} - -pub(crate) unsafe fn set_fallback_reason(state: &mut HookState, reason: u32) { - (*state.info).fallback_reason = reason; -} - -pub(crate) unsafe fn set_capture_flags(state: &mut HookState, flags: u32) { - (*state.info).capture_flags = flags; -} - -static VERBOSE_LOG_PATH: std::sync::OnceLock> = - std::sync::OnceLock::new(); - -fn verbose_log_path() -> &'static Option { - VERBOSE_LOG_PATH.get_or_init(|| { - if !env_flag_enabled(ENV_VERBOSE) { - return None; - } - let mut path = std::env::temp_dir(); - path.push(format!("fluxer-game-hook-{}.log", current_pid())); - Some(path) - }) -} - -pub(crate) fn verbose_log(message: &str) { - let Some(path) = verbose_log_path() else { - return; - }; - let text = format!("[fluxer-game-hook] {message}"); - - { - use std::io::Write; - if let Ok(mut file) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path) - { - let _ = writeln!( - file, - "[{} pid={}] {text}", - now_ns() / 1_000_000, - current_pid() - ); - } - } - - let line = wide(&text); - unsafe { - windows_sys::Win32::System::Diagnostics::Debug::OutputDebugStringW(line.as_ptr()); - } - use std::io::Write; - let _ = writeln!(std::io::stderr(), "{text}"); -} - -pub(crate) unsafe fn capture_should_run(state: &mut HookState) -> bool { - if WaitForSingleObject(state.stop_event, 0) == WAIT_OBJECT_0 { - (*state.info).state = GAME_CAPTURE_STATE_STOPPED; - return false; - } - if !parent_alive(&state.keepalive_name) { - (*state.info).state = GAME_CAPTURE_STATE_STOPPED; - return false; - } - true -} - -unsafe fn client_size_from_hdc(hdc: HDC) -> Option<(HWND, u32, u32)> { - let hwnd = WindowFromDC(hdc); - if hwnd.is_null() || IsWindow(hwnd) == 0 { - return None; - } - let mut rect = mem::zeroed(); - if GetClientRect(hwnd, &mut rect) == 0 { - return None; - } - let width = (rect.right - rect.left).max(1) as u32; - let height = (rect.bottom - rect.top).max(1) as u32; - Some((hwnd, width, height)) -} - -pub(crate) unsafe fn frame_due(state: &mut HookState) -> bool { - let info = &*state.info; - let interval = info.target_frame_interval_ns; - if interval == 0 { - return true; - } - let now = now_ns(); - if now.saturating_sub(state.last_frame_ns) < interval { - return false; - } - state.last_frame_ns = now; - true -} - -pub(crate) unsafe fn write_bgra_rows_to_shared_memory( - state: &mut HookState, - hwnd: HWND, - width: u32, - height: u32, - src_base: *const u8, - src_row_pitch: usize, - flip_vertical: bool, - format_mode: D3d11FormatMode, -) -> bool { - if src_base.is_null() { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 2; - return false; - } - let max_width = (*state.info).max_width; - let max_height = (*state.info).max_height; - if width == 0 || height == 0 || width > max_width || height > max_height { - (*state.info).state = GAME_CAPTURE_STATE_RESIZE_REQUIRED; - (*state.info).width = width; - (*state.info).height = height; - return false; - } - let Some(bytes) = frame_buffer_size(width, height) else { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 1; - return false; - }; - if bytes > state.frame_buffer_capacity { - (*state.info).state = GAME_CAPTURE_STATE_RESIZE_REQUIRED; - (*state.info).width = width; - (*state.info).height = height; - return false; - } - - let mut index = state.next_frame_index; - let mut mutex = state.mutexes[index]; - let mut wait = WaitForSingleObject(mutex, 0); - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - index = (index + 1) % GAME_CAPTURE_BUFFER_COUNT; - mutex = state.mutexes[index]; - wait = WaitForSingleObject(mutex, 0); - } - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - record_dropped_frame(state); - return false; - } - - let row_bytes = (width * 4) as usize; - let dst_base = state.frame_base.add(state.frame_buffer_capacity * index); - for y in 0..height as usize { - let src_y = if flip_vertical { - height as usize - 1 - y - } else { - y - }; - let src = src_base.add(src_y * src_row_pitch); - let dst = dst_base.add(y * row_bytes); - match format_mode { - D3d11FormatMode::Bgra => std::ptr::copy_nonoverlapping(src, dst, row_bytes), - D3d11FormatMode::Rgba => { - for x in 0..width as usize { - let src_px = src.add(x * 4); - let dst_px = dst.add(x * 4); - *dst_px.add(0) = *src_px.add(2); - *dst_px.add(1) = *src_px.add(1); - *dst_px.add(2) = *src_px.add(0); - *dst_px.add(3) = *src_px.add(3); - } - } - } - } - - let info = &mut *state.info; - info.hwnd = hwnd as usize as u64; - info.width = width; - info.height = height; - info.pitch = width * 4; - info.frame_index = index as u32; - info.frame_counter = info.frame_counter.wrapping_add(1); - info.timestamp_us = now_us(); - info.transport = GAME_CAPTURE_TRANSPORT_MEMORY; - info.dxgi_format = 0; - info.texture_handle = 0; - info.state = GAME_CAPTURE_STATE_ACTIVE; - state.next_frame_index = (index + 1) % GAME_CAPTURE_BUFFER_COUNT; - - ReleaseMutex(mutex); - SetEvent(state.ready_event); - true -} - -pub(crate) unsafe fn publish_shared_texture_frame( - state: &mut HookState, - hwnd: HWND, - width: u32, - height: u32, - dxgi_format: DXGI_FORMAT, - shared_handle: u64, -) -> bool { - let max_width = (*state.info).max_width; - let max_height = (*state.info).max_height; - if width == 0 || height == 0 || width > max_width || height > max_height { - (*state.info).state = GAME_CAPTURE_STATE_RESIZE_REQUIRED; - (*state.info).width = width; - (*state.info).height = height; - return false; - } - if shared_handle == 0 { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 40; - return false; - } - - let info = &mut *state.info; - info.hwnd = hwnd as usize as u64; - info.width = width; - info.height = height; - info.pitch = width * 4; - info.frame_index = 0; - info.frame_counter = info.frame_counter.wrapping_add(1); - info.timestamp_us = now_us(); - info.transport = GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE; - info.dxgi_format = dxgi_format.0 as u32; - info.texture_handle = shared_handle; - info.state = GAME_CAPTURE_STATE_ACTIVE; - SetEvent(state.ready_event); - true -} - -unsafe fn gl_bind_buffer_proc() -> Option { - static GL_BIND_BUFFER: OnceLock> = OnceLock::new(); - *GL_BIND_BUFFER.get_or_init(|| { - let Some(proc) = wglGetProcAddress(c"glBindBuffer".as_ptr().cast()) else { - return None; - }; - let address = proc as usize; - if address <= 3 || address == usize::MAX { - return None; - } - Some(mem::transmute::< - unsafe extern "system" fn() -> isize, - GlBindBufferFn, - >(proc)) - }) -} - -unsafe fn write_frame_to_shared_memory( - state: &mut HookState, - hwnd: HWND, - width: u32, - height: u32, -) -> bool { - let Some(bytes) = frame_buffer_size(width, height) else { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 1; - return false; - }; - if state.scratch.len() != bytes { - state.scratch.resize(bytes, 0); - } - - let mut previous_read_buffer = 0i32; - let mut prev_alignment = 4i32; - let mut prev_row_length = 0i32; - let mut prev_skip_pixels = 0i32; - let mut prev_skip_rows = 0i32; - let mut prev_swap_bytes = 0i32; - let mut prev_lsb_first = 0i32; - let mut prev_pack_buffer = 0i32; - glGetIntegerv(GL_READ_BUFFER, &mut previous_read_buffer); - glGetIntegerv(GL_PACK_ALIGNMENT, &mut prev_alignment); - glGetIntegerv(GL_PACK_ROW_LENGTH, &mut prev_row_length); - glGetIntegerv(GL_PACK_SKIP_PIXELS, &mut prev_skip_pixels); - glGetIntegerv(GL_PACK_SKIP_ROWS, &mut prev_skip_rows); - glGetIntegerv(GL_PACK_SWAP_BYTES, &mut prev_swap_bytes); - glGetIntegerv(GL_PACK_LSB_FIRST, &mut prev_lsb_first); - glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &mut prev_pack_buffer); - - glReadBuffer(GL_BACK); - let bind_buffer = if prev_pack_buffer != 0 { - match gl_bind_buffer_proc() { - Some(bind_buffer) => { - bind_buffer(GL_PIXEL_PACK_BUFFER, 0); - Some(bind_buffer) - } - None => { - record_dropped_frame(state); - return false; - } - } - } else { - None - }; - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glPixelStorei(GL_PACK_ROW_LENGTH, 0); - glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - glPixelStorei(GL_PACK_SKIP_ROWS, 0); - glPixelStorei(GL_PACK_SWAP_BYTES, 0); - glPixelStorei(GL_PACK_LSB_FIRST, 0); - glReadPixels( - 0, - 0, - width as i32, - height as i32, - GL_BGRA_EXT, - GL_UNSIGNED_BYTE, - state.scratch.as_mut_ptr().cast(), - ); - - glReadBuffer(previous_read_buffer as u32); - glPixelStorei(GL_PACK_ALIGNMENT, prev_alignment); - glPixelStorei(GL_PACK_ROW_LENGTH, prev_row_length); - glPixelStorei(GL_PACK_SKIP_PIXELS, prev_skip_pixels); - glPixelStorei(GL_PACK_SKIP_ROWS, prev_skip_rows); - glPixelStorei(GL_PACK_SWAP_BYTES, prev_swap_bytes); - glPixelStorei(GL_PACK_LSB_FIRST, prev_lsb_first); - if let Some(bind_buffer) = bind_buffer { - bind_buffer(GL_PIXEL_PACK_BUFFER, prev_pack_buffer as u32); - } - - write_bgra_rows_to_shared_memory( - state, - hwnd, - width, - height, - state.scratch.as_ptr(), - (width * 4) as usize, - true, - D3d11FormatMode::Bgra, - ) -} - -unsafe fn capture_opengl_frame(hdc: HDC) { - if !HOOKS_READY.load(Ordering::Acquire) { - return; - } - let should_continue = { - let Some(state) = ensure_state() else { - return; - }; - capture_should_run(state) - }; - if !should_continue { - clear_state(); - return; - } - let Some(state) = ensure_state() else { - return; - }; - mark_present(state, GAME_CAPTURE_API_OPENGL); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_NONE); - if !frame_due(state) { - record_dropped_frame(state); - return; - } - let Some((hwnd, width, height)) = client_size_from_hdc(hdc) else { - return; - }; - - let force_cpu = env_flag_enabled(ENV_FORCE_CPU); - let enable_shared_texture = env_flag_enabled(ENV_ENABLE_OPENGL_SHARED_TEXTURE) - || env_flag_enabled(ENV_FORCE_SHARED_TEXTURE); - let disable_shared_texture = !enable_shared_texture - || env_flag_enabled(ENV_DISABLE_OPENGL_SHARED_TEXTURE) - || ((*state.info).control & GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE) != 0; - if disable_shared_texture { - if state.gl_interop.is_some() { - verbose_log("opengl interop: shared texture disabled; using memory fallback"); - } - state.gl_interop = None; - } - - if !force_cpu - && !disable_shared_texture - && !gl_interop::gpu_path_disabled() - && gl_interop::capture_opengl_frame_gpu(state, hwnd, width, height) - { - return; - } - - set_capture_flags(state, GAME_CAPTURE_FLAG_FLIP_VERTICAL); - if force_cpu { - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORCED_CPU); - } else if disable_shared_texture || gl_interop::gpu_path_disabled() { - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_SHARED_TEXTURE_UNSUPPORTED); - } - let _ = write_frame_to_shared_memory(state, hwnd, width, height); -} - -fn d3d11_format_mode(format: DXGI_FORMAT) -> Option { - if format == DXGI_FORMAT_B8G8R8A8_UNORM - || format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB - || format == DXGI_FORMAT_B8G8R8X8_UNORM - || format == DXGI_FORMAT_B8G8R8X8_UNORM_SRGB - { - Some(D3d11FormatMode::Bgra) - } else if format == DXGI_FORMAT_R8G8B8A8_UNORM || format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB { - Some(D3d11FormatMode::Rgba) - } else { - None - } -} - -fn dxgi_hdr_flags(format: DXGI_FORMAT) -> u32 { - if format == DXGI_FORMAT_R10G10B10A2_UNORM || format == DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM { - GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR - } else if format == DXGI_FORMAT_R16G16B16A16_FLOAT || format == DXGI_FORMAT_R16G16B16A16_UNORM { - GAME_CAPTURE_FLAG_HDR - } else { - 0 - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum DxgiCapturePlan { - Sdr8 { format_mode: D3d11FormatMode }, - HdrShared { flags: u32 }, - Unsupported, -} - -fn dxgi_capture_plan(format: DXGI_FORMAT, force_cpu: bool) -> DxgiCapturePlan { - if let Some(format_mode) = d3d11_format_mode(format) { - return DxgiCapturePlan::Sdr8 { format_mode }; - } - let hdr_flags = dxgi_hdr_flags(format); - if hdr_flags != 0 { - if force_cpu { - return DxgiCapturePlan::Unsupported; - } - return DxgiCapturePlan::HdrShared { flags: hdr_flags }; - } - DxgiCapturePlan::Unsupported -} - -unsafe fn publish_d3d11_staging_cpu( - state: &mut HookState, - context: &ID3D11DeviceContext, - staging: &ID3D11Texture2D, - hwnd: HWND, - width: u32, - height: u32, - format_mode: D3d11FormatMode, -) -> bool { - use windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE; - let resource: ID3D11Resource = match staging.cast() { - Ok(resource) => resource, - Err(_) => { - record_dropped_frame(state); - return false; - } - }; - let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - if context - .Map(&resource, 0, D3D11_MAP_READ, 0, Some(&mut mapped)) - .is_err() - { - record_dropped_frame(state); - return false; - } - let published = write_bgra_rows_to_shared_memory( - state, - hwnd, - width, - height, - mapped.pData as *const u8, - mapped.RowPitch as usize, - false, - format_mode, - ); - context.Unmap(&resource, 0); - published -} - -unsafe fn d3d11_state_for_frame<'a>( - state: &'a mut HookState, - device_ptr: usize, - context: ID3D11DeviceContext, - device: &windows::Win32::Graphics::Direct3D11::ID3D11Device, - desc: &D3D11_TEXTURE2D_DESC, - format_mode: D3d11FormatMode, - force_cpu: bool, -) -> Option<&'a mut D3d11CaptureState> { - let sample_count = desc.SampleDesc.Count.max(1); - let recreate = state - .d3d11 - .as_ref() - .map(|capture| { - capture.device_ptr != device_ptr - || capture.width != desc.Width - || capture.height != desc.Height - || capture.format != desc.Format - || capture.sample_count != sample_count - || capture.force_cpu != force_cpu - }) - .unwrap_or(true); - if recreate { - let mut copy_desc = *desc; - copy_desc.MipLevels = 1; - copy_desc.ArraySize = 1; - copy_desc.SampleDesc.Count = 1; - copy_desc.SampleDesc.Quality = 0; - copy_desc.Usage = D3D11_USAGE_DEFAULT; - copy_desc.BindFlags = (D3D11_BIND_SHADER_RESOURCE.0 | D3D11_BIND_RENDER_TARGET.0) as u32; - copy_desc.CPUAccessFlags = 0; - copy_desc.MiscFlags = if force_cpu { - 0 - } else { - D3D11_RESOURCE_MISC_SHARED.0 as u32 - }; - let mut texture = None; - if device - .CreateTexture2D(©_desc, None, Some(&mut texture)) - .is_err() - { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 20; - return None; - } - let Some(texture) = texture else { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 20; - return None; - }; - - let (staging, shared_handle) = if force_cpu { - let mut staging_desc = copy_desc; - staging_desc.Usage = D3D11_USAGE_STAGING; - staging_desc.BindFlags = 0; - staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32; - staging_desc.MiscFlags = 0; - let mut staging = None; - if device - .CreateTexture2D(&staging_desc, None, Some(&mut staging)) - .is_err() - { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 22; - return None; - } - (staging, 0u64) - } else { - let handle = match texture - .cast::() - .and_then(|resource| resource.GetSharedHandle()) - { - Ok(handle) => handle.0 as usize as u64, - Err(_) => { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 21; - return None; - } - }; - (None, handle) - }; - - state.d3d11 = Some(D3d11CaptureState { - device_ptr, - context, - texture, - staging, - shared_handle, - width: desc.Width, - height: desc.Height, - format: desc.Format, - format_mode, - sample_count, - force_cpu, - }); - } - state.d3d11.as_mut() -} - -unsafe fn capture_d3d11_frame(swap_chain: *mut c_void) { - if !HOOKS_READY.load(Ordering::Acquire) { - return; - } - let should_continue = { - let Some(state) = ensure_state() else { - return; - }; - capture_should_run(state) - }; - if !should_continue { - clear_state(); - return; - } - let Some(state) = ensure_state() else { - return; - }; - - let Some(swap_chain) = IDXGISwapChain::from_raw_borrowed(&swap_chain) else { - return; - }; - let swap_desc = swap_chain.GetDesc().ok(); - let hwnd = swap_desc - .as_ref() - .map(|desc| desc.OutputWindow.0 as HWND) - .filter(|hwnd| !hwnd.is_null()) - .unwrap_or((*state.info).hwnd as usize as HWND); - - if swap_chain.GetDevice::().is_ok() { - capture_d3d10_frame_impl(state, swap_chain, hwnd); - return; - } - - let Ok(back_buffer) = swap_chain.GetBuffer::(0) else { - return; - }; - let mut desc = D3D11_TEXTURE2D_DESC::default(); - back_buffer.GetDesc(&mut desc); - if desc.Width == 0 || desc.Height == 0 { - return; - } - let multisampled = desc.SampleDesc.Count > 1; - let force_cpu = env_flag_enabled(ENV_FORCE_CPU); - - let (format_mode, hdr_flags) = match dxgi_capture_plan(desc.Format, force_cpu) { - DxgiCapturePlan::Sdr8 { format_mode } => (format_mode, 0u32), - DxgiCapturePlan::HdrShared { flags } => (D3d11FormatMode::Bgra, flags), - DxgiCapturePlan::Unsupported => { - mark_present(state, GAME_CAPTURE_API_D3D11); - set_capture_flags(state, dxgi_hdr_flags(desc.Format)); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED); - record_dropped_frame(state); - return; - } - }; - - mark_present(state, GAME_CAPTURE_API_D3D11); - let mut flags = hdr_flags; - if multisampled { - flags |= GAME_CAPTURE_FLAG_MULTISAMPLED; - } - set_capture_flags(state, flags); - set_fallback_reason( - state, - if force_cpu { - GAME_CAPTURE_FALLBACK_FORCED_CPU - } else { - GAME_CAPTURE_FALLBACK_NONE - }, - ); - - if !frame_due(state) { - record_dropped_frame(state); - return; - } - let Ok(device) = back_buffer.GetDevice() else { - record_dropped_frame(state); - return; - }; - let Ok(context) = device.GetImmediateContext() else { - record_dropped_frame(state); - return; - }; - let device_ptr = device.as_raw() as usize; - let (context, texture, staging, shared_handle) = { - let Some(capture_state) = d3d11_state_for_frame( - state, - device_ptr, - context, - &device, - &desc, - format_mode, - force_cpu, - ) else { - record_dropped_frame(state); - return; - }; - ( - capture_state.context.clone(), - capture_state.texture.clone(), - capture_state.staging.clone(), - capture_state.shared_handle, - ) - }; - - if multisampled { - context.ResolveSubresource(&texture, 0, &back_buffer, 0, desc.Format); - } else { - context.CopyResource(&texture, &back_buffer); - } - - if force_cpu { - let Some(staging) = staging else { - record_dropped_frame(state); - return; - }; - context.CopyResource(&staging, &texture); - context.Flush(); - let _ = publish_d3d11_staging_cpu( - state, - &context, - &staging, - hwnd, - desc.Width, - desc.Height, - format_mode, - ); - return; - } - - context.Flush(); - let _ = publish_shared_texture_frame( - state, - hwnd, - desc.Width, - desc.Height, - desc.Format, - shared_handle, - ); -} - -unsafe fn d3d10_state_for_frame<'a>( - state: &'a mut HookState, - device_ptr: usize, - device: &ID3D10Device, - desc: &D3D10_TEXTURE2D_DESC, - format_mode: D3d11FormatMode, - force_cpu: bool, -) -> Option<&'a mut D3d10CaptureState> { - let sample_count = desc.SampleDesc.Count.max(1); - let recreate = state - .d3d10 - .as_ref() - .map(|capture| { - capture.device_ptr != device_ptr - || capture.width != desc.Width - || capture.height != desc.Height - || capture.format != desc.Format - || capture.sample_count != sample_count - || capture.force_cpu != force_cpu - }) - .unwrap_or(true); - if recreate { - let mut copy_desc = *desc; - copy_desc.MipLevels = 1; - copy_desc.ArraySize = 1; - copy_desc.SampleDesc.Count = 1; - copy_desc.SampleDesc.Quality = 0; - copy_desc.Usage = D3D10_USAGE_DEFAULT; - copy_desc.BindFlags = (D3D10_BIND_SHADER_RESOURCE.0 | D3D10_BIND_RENDER_TARGET.0) as u32; - copy_desc.CPUAccessFlags = 0; - copy_desc.MiscFlags = if force_cpu { - 0 - } else { - D3D10_RESOURCE_MISC_SHARED.0 as u32 - }; - let texture = match device.CreateTexture2D(©_desc, None) { - Ok(texture) => texture, - Err(_) => { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 50; - return None; - } - }; - - let (staging, shared_handle) = if force_cpu { - use windows::Win32::Graphics::Direct3D10::{ - D3D10_CPU_ACCESS_READ, D3D10_USAGE_STAGING, - }; - let mut staging_desc = copy_desc; - staging_desc.Usage = D3D10_USAGE_STAGING; - staging_desc.BindFlags = 0; - staging_desc.CPUAccessFlags = D3D10_CPU_ACCESS_READ.0 as u32; - staging_desc.MiscFlags = 0; - let staging = match device.CreateTexture2D(&staging_desc, None) { - Ok(staging) => staging, - Err(_) => { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 52; - return None; - } - }; - (Some(staging), 0u64) - } else { - let handle = match texture - .cast::() - .and_then(|resource| resource.GetSharedHandle()) - { - Ok(handle) => handle.0 as usize as u64, - Err(_) => { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 51; - return None; - } - }; - (None, handle) - }; - - state.d3d10 = Some(D3d10CaptureState { - device_ptr, - device: device.clone(), - texture, - staging, - shared_handle, - width: desc.Width, - height: desc.Height, - format: desc.Format, - format_mode, - sample_count, - force_cpu, - }); - } - state.d3d10.as_mut() -} - -unsafe fn publish_d3d10_staging_cpu( - state: &mut HookState, - staging: &ID3D10Texture2D, - hwnd: HWND, - width: u32, - height: u32, - format_mode: D3d11FormatMode, -) -> bool { - use windows::Win32::Graphics::Direct3D10::D3D10_MAP_READ; - let mapped = match staging.Map(0, D3D10_MAP_READ, 0) { - Ok(mapped) => mapped, - Err(_) => { - record_dropped_frame(state); - return false; - } - }; - let published = write_bgra_rows_to_shared_memory( - state, - hwnd, - width, - height, - mapped.pData as *const u8, - mapped.RowPitch as usize, - false, - format_mode, - ); - staging.Unmap(0); - published -} - -unsafe fn capture_d3d10_frame_impl(state: &mut HookState, swap_chain: &IDXGISwapChain, hwnd: HWND) { - let Ok(back_buffer) = swap_chain.GetBuffer::(0) else { - return; - }; - let mut desc = D3D10_TEXTURE2D_DESC::default(); - back_buffer.GetDesc(&mut desc); - if desc.Width == 0 || desc.Height == 0 { - return; - } - let multisampled = desc.SampleDesc.Count > 1; - let force_cpu = env_flag_enabled(ENV_FORCE_CPU); - - let (format_mode, hdr_flags) = match dxgi_capture_plan(desc.Format, force_cpu) { - DxgiCapturePlan::Sdr8 { format_mode } => (format_mode, 0u32), - DxgiCapturePlan::HdrShared { flags } => (D3d11FormatMode::Bgra, flags), - DxgiCapturePlan::Unsupported => { - mark_present(state, GAME_CAPTURE_API_D3D10); - set_capture_flags(state, dxgi_hdr_flags(desc.Format)); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED); - record_dropped_frame(state); - return; - } - }; - - mark_present(state, GAME_CAPTURE_API_D3D10); - let mut flags = hdr_flags; - if multisampled { - flags |= GAME_CAPTURE_FLAG_MULTISAMPLED; - } - set_capture_flags(state, flags); - set_fallback_reason( - state, - if force_cpu { - GAME_CAPTURE_FALLBACK_FORCED_CPU - } else { - GAME_CAPTURE_FALLBACK_NONE - }, - ); - - if !frame_due(state) { - record_dropped_frame(state); - return; - } - - let Ok(device) = swap_chain.GetDevice::() else { - record_dropped_frame(state); - return; - }; - let device_ptr = device.as_raw() as usize; - let (device, texture, staging, shared_handle) = { - let Some(capture_state) = - d3d10_state_for_frame(state, device_ptr, &device, &desc, format_mode, force_cpu) - else { - record_dropped_frame(state); - return; - }; - ( - capture_state.device.clone(), - capture_state.texture.clone(), - capture_state.staging.clone(), - capture_state.shared_handle, - ) - }; - - if multisampled { - device.ResolveSubresource(&texture, 0, &back_buffer, 0, desc.Format); - } else { - device.CopyResource(&texture, &back_buffer); - } - - if force_cpu { - let Some(staging) = staging else { - record_dropped_frame(state); - return; - }; - device.CopyResource(&staging, &texture); - device.Flush(); - let _ = - publish_d3d10_staging_cpu(state, &staging, hwnd, desc.Width, desc.Height, format_mode); - return; - } - - device.Flush(); - let _ = publish_shared_texture_frame( - state, - hwnd, - desc.Width, - desc.Height, - desc.Format, - shared_handle, - ); -} - -unsafe fn reset_d3d12_queue_candidates() { - if let Ok(mut candidates) = D3D12_QUEUE_CANDIDATES.lock() { - candidates.clear(); - } - DXGI_PRESENT_ATTEMPTED.store(false, Ordering::Release); -} - -unsafe fn remember_d3d12_queue_candidate(queue: *mut c_void) { - if queue.is_null() { - return; - } - let Some(queue_ref) = ID3D12CommandQueue::from_raw_borrowed(&queue) else { - return; - }; - if queue_ref.GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT { - return; - } - let Ok(mut candidates) = D3D12_QUEUE_CANDIDATES.lock() else { - return; - }; - if candidates - .iter() - .any(|candidate| candidate.as_raw() == queue) - { - return; - } - if candidates.len() < MAX_D3D12_QUEUE_CANDIDATES { - candidates.push(queue_ref.clone()); - } -} - -unsafe fn create_d3d11_on_d3d12( - device12: &ID3D12Device, -) -> Option<(ID3D11Device, ID3D11DeviceContext, ID3D11On12Device)> { - let Ok(device_unknown) = device12.cast::() else { - return None; - }; - let candidates = D3D12_QUEUE_CANDIDATES.lock().ok()?.clone(); - for queue in candidates { - if queue.GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT { - continue; - } - let Ok(queue_unknown) = queue.cast::() else { - continue; - }; - let queues = [Some(queue_unknown)]; - let mut device11 = None; - let mut context11 = None; - if D3D11On12CreateDevice( - &device_unknown, - 0, - None, - Some(&queues), - 0, - Some(&mut device11), - Some(&mut context11), - None, - ) - .is_err() - { - continue; - } - let Some(device11) = device11 else { - continue; - }; - let Some(context11) = context11 else { - continue; - }; - let Ok(device11on12) = device11.cast::() else { - continue; - }; - return Some((device11, context11, device11on12)); - } - None -} - -unsafe fn d3d12_backbuffer_count(desc: &DXGI_SWAP_CHAIN_DESC) -> u32 { - if desc.SwapEffect == DXGI_SWAP_EFFECT_DISCARD { - 1 - } else { - desc.BufferCount.max(1) - } -} - -unsafe fn d3d12_state_for_frame<'a>( - state: &'a mut HookState, - swap_chain: &IDXGISwapChain, - device_ptr: usize, - device12: &ID3D12Device, - desc: &DXGI_SWAP_CHAIN_DESC, - format_mode: D3d11FormatMode, - force_cpu: bool, -) -> Option<&'a mut D3d12CaptureState> { - let backbuffer_count = d3d12_backbuffer_count(desc); - let recreate = state - .d3d12 - .as_ref() - .map(|capture| { - capture.device_ptr != device_ptr - || capture.width != desc.BufferDesc.Width - || capture.height != desc.BufferDesc.Height - || capture.format != desc.BufferDesc.Format - || capture.backbuffer_count != backbuffer_count - || capture.force_cpu != force_cpu - }) - .unwrap_or(true); - if recreate { - let Some((device11, context11, device11on12)) = create_d3d11_on_d3d12(device12) else { - return None; - }; - let copy_desc = D3D11_TEXTURE2D_DESC { - Width: desc.BufferDesc.Width, - Height: desc.BufferDesc.Height, - MipLevels: 1, - ArraySize: 1, - Format: desc.BufferDesc.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: if force_cpu { - 0 - } else { - D3D11_RESOURCE_MISC_SHARED.0 as u32 - }, - }; - let mut copy_tex = None; - if device11 - .CreateTexture2D(©_desc, None, Some(&mut copy_tex)) - .is_err() - { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 30; - return None; - } - state.d3d12 = match copy_tex { - Some(copy_tex) => { - let (staging, shared_handle) = if force_cpu { - let mut staging_desc = copy_desc; - staging_desc.Usage = D3D11_USAGE_STAGING; - staging_desc.BindFlags = 0; - staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32; - staging_desc.MiscFlags = 0; - let mut staging = None; - if device11 - .CreateTexture2D(&staging_desc, None, Some(&mut staging)) - .is_err() - { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 32; - return None; - } - (staging, 0u64) - } else { - let handle = match copy_tex - .cast::() - .and_then(|resource| resource.GetSharedHandle()) - { - Ok(handle) => handle.0 as usize as u64, - Err(_) => { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 31; - return None; - } - }; - (None, handle) - }; - Some(D3d12CaptureState { - device_ptr, - device11, - context11, - device11on12, - copy_tex, - staging, - shared_handle, - width: desc.BufferDesc.Width, - height: desc.BufferDesc.Height, - format: desc.BufferDesc.Format, - format_mode, - backbuffer_count, - current_backbuffer: 0, - dxgi14: backbuffer_count > 1 && swap_chain.cast::().is_ok(), - force_cpu, - }) - } - _ => None, - }; - } - state.d3d12.as_mut() -} - -unsafe fn capture_d3d12_frame(swap_chain: *mut c_void) { - if !HOOKS_READY.load(Ordering::Acquire) { - return; - } - if env_flag_enabled(ENV_DISABLE_D3D12) { - return; - } - let should_continue = { - let Some(state) = ensure_state() else { - return; - }; - capture_should_run(state) - }; - if !should_continue { - clear_state(); - return; - } - let Some(state) = ensure_state() else { - return; - }; - - let Some(swap_chain) = IDXGISwapChain::from_raw_borrowed(&swap_chain) else { - return; - }; - let Ok(desc) = swap_chain.GetDesc() else { - return; - }; - let Ok(device12) = swap_chain.GetDevice::() else { - return; - }; - if desc.BufferDesc.Width == 0 || desc.BufferDesc.Height == 0 { - return; - } - if desc.SampleDesc.Count != 1 { - mark_present(state, GAME_CAPTURE_API_D3D12); - set_capture_flags(state, GAME_CAPTURE_FLAG_MULTISAMPLED); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_MULTISAMPLED); - record_dropped_frame(state); - return; - } - let force_cpu = env_flag_enabled(ENV_FORCE_CPU); - let (format_mode, hdr_flags) = match dxgi_capture_plan(desc.BufferDesc.Format, force_cpu) { - DxgiCapturePlan::Sdr8 { format_mode } => (format_mode, 0u32), - DxgiCapturePlan::HdrShared { flags } => (D3d11FormatMode::Bgra, flags), - DxgiCapturePlan::Unsupported => { - mark_present(state, GAME_CAPTURE_API_D3D12); - set_capture_flags(state, dxgi_hdr_flags(desc.BufferDesc.Format)); - set_fallback_reason(state, GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED); - record_dropped_frame(state); - return; - } - }; - let device_ptr = device12.as_raw() as usize; - - mark_present(state, GAME_CAPTURE_API_D3D12); - set_capture_flags(state, hdr_flags); - set_fallback_reason( - state, - if force_cpu { - GAME_CAPTURE_FALLBACK_FORCED_CPU - } else { - GAME_CAPTURE_FALLBACK_NONE - }, - ); - - if !frame_due(state) { - record_dropped_frame(state); - return; - } - let (context11, device11on12, copy_tex, staging, shared_handle, backbuffer_index) = { - let Some(capture_state) = d3d12_state_for_frame( - state, - swap_chain, - device_ptr, - &device12, - &desc, - format_mode, - force_cpu, - ) else { - record_dropped_frame(state); - return; - }; - let index = if capture_state.dxgi14 { - swap_chain - .cast::() - .ok() - .map(|swap_chain3| swap_chain3.GetCurrentBackBufferIndex()) - .unwrap_or(0) - } else { - capture_state.current_backbuffer - }; - if !capture_state.dxgi14 { - capture_state.current_backbuffer = - (capture_state.current_backbuffer + 1) % capture_state.backbuffer_count.max(1); - } - ( - capture_state.context11.clone(), - capture_state.device11on12.clone(), - capture_state.copy_tex.clone(), - capture_state.staging.clone(), - capture_state.shared_handle, - index, - ) - }; - - let Ok(backbuffer12) = swap_chain.GetBuffer::(backbuffer_index) else { - record_dropped_frame(state); - return; - }; - let mut wrapped: Option = None; - let resource_flags = D3D11_RESOURCE_FLAGS::default(); - if device11on12 - .CreateWrappedResource( - &backbuffer12, - &resource_flags, - D3D12_RESOURCE_STATE_PRESENT, - D3D12_RESOURCE_STATE_PRESENT, - &mut wrapped, - ) - .is_err() - { - record_dropped_frame(state); - return; - } - let Some(wrapped) = wrapped else { - record_dropped_frame(state); - return; - }; - let wrapped_resources = [Some(wrapped.clone())]; - device11on12.AcquireWrappedResources(&wrapped_resources); - context11.CopyResource(©_tex, &wrapped); - device11on12.ReleaseWrappedResources(&wrapped_resources); - - let hwnd = desc.OutputWindow.0 as HWND; - if force_cpu { - let Some(staging) = staging else { - record_dropped_frame(state); - return; - }; - context11.CopyResource(&staging, ©_tex); - context11.Flush(); - let _ = publish_d3d11_staging_cpu( - state, - &context11, - &staging, - hwnd, - desc.BufferDesc.Width, - desc.BufferDesc.Height, - format_mode, - ); - return; - } - - context11.Flush(); - let _ = publish_shared_texture_frame( - state, - hwnd, - desc.BufferDesc.Width, - desc.BufferDesc.Height, - desc.BufferDesc.Format, - shared_handle, - ); -} - -unsafe extern "system" fn swap_buffers_detour(hdc: HDC) -> BOOL { - if !OPENGL_PRESENT_FIRED.swap(true, Ordering::AcqRel) { - verbose_log("opengl SwapBuffers detour fired (first present)"); - } - capture_opengl_frame(hdc); - if let Some(hook) = SWAP_BUFFERS_HOOK.as_ref() { - return (hook.trampoline_fn())(hdc); - } - 0 -} - -unsafe extern "system" fn wgl_swap_layer_buffers_detour(hdc: HDC, flags: u32) -> BOOL { - capture_opengl_frame(hdc); - if let Some(hook) = WGL_SWAP_LAYER_BUFFERS_HOOK.as_ref() { - return (hook.trampoline_fn())(hdc, flags); - } - 0 -} - -unsafe extern "system" fn dxgi_present_detour( - swap_chain: *mut c_void, - sync_interval: u32, - flags: u32, -) -> HRESULT { - let suppress_capture = gl_interop::dummy_present_active(); - if !suppress_capture { - if !DXGI_PRESENT_FIRED.swap(true, Ordering::AcqRel) { - verbose_log("dxgi Present detour fired (first present)"); - } - capture_d3d11_frame(swap_chain); - capture_d3d12_frame(swap_chain); - } - if let Some(hook) = DXGI_PRESENT_HOOK.as_ref() { - if !suppress_capture { - DXGI_PRESENT_DEPTH.fetch_add(1, Ordering::AcqRel); - } - let result = (hook.trampoline_fn())(swap_chain, sync_interval, flags); - if !suppress_capture { - DXGI_PRESENT_DEPTH.fetch_sub(1, Ordering::AcqRel); - DXGI_PRESENT_ATTEMPTED.store(true, Ordering::Release); - } - return result; - } - HRESULT(0) -} - -unsafe extern "system" fn dxgi_present1_detour( - swap_chain: *mut c_void, - sync_interval: u32, - flags: u32, - present_parameters: *const DXGI_PRESENT_PARAMETERS, -) -> HRESULT { - let suppress_capture = gl_interop::dummy_present_active(); - if !suppress_capture { - if !DXGI_PRESENT1_FIRED.swap(true, Ordering::AcqRel) { - verbose_log("dxgi Present1 detour fired (first present)"); - } - capture_d3d11_frame(swap_chain); - capture_d3d12_frame(swap_chain); - } - if let Some(hook) = DXGI_PRESENT1_HOOK.as_ref() { - if !suppress_capture { - DXGI_PRESENT_DEPTH.fetch_add(1, Ordering::AcqRel); - } - let result = (hook.trampoline_fn())(swap_chain, sync_interval, flags, present_parameters); - if !suppress_capture { - DXGI_PRESENT_DEPTH.fetch_sub(1, Ordering::AcqRel); - DXGI_PRESENT_ATTEMPTED.store(true, Ordering::Release); - } - return result; - } - HRESULT(0) -} - -unsafe extern "system" fn d3d12_execute_command_lists_detour( - queue: *mut c_void, - num_command_lists: u32, - command_lists: *const *mut c_void, -) { - if !D3D12_EXECUTE_FIRED.swap(true, Ordering::AcqRel) { - verbose_log("d3d12 ExecuteCommandLists detour fired (first call)"); - } - if DXGI_PRESENT_DEPTH.load(Ordering::Acquire) > 0 - || DXGI_PRESENT_ATTEMPTED.load(Ordering::Acquire) - { - remember_d3d12_queue_candidate(queue); - } - if let Some(hook) = D3D12_EXECUTE_COMMAND_LISTS_HOOK.as_ref() { - (hook.trampoline_fn())(queue, num_command_lists, command_lists); - } -} - -unsafe extern "system" fn dummy_window_proc( - hwnd: HWND, - msg: u32, - wparam: usize, - lparam: isize, -) -> isize { - DefWindowProcW(hwnd, msg, wparam, lparam) -} - -unsafe fn module_loaded(module: &str) -> bool { - let name = wide(module); - !GetModuleHandleW(name.as_ptr()).is_null() -} - -unsafe fn log_loaded_graphics_modules(tag: &str) { - if !env_flag_enabled(ENV_VERBOSE) { - return; - } - verbose_log(&format!( - "{tag} loaded modules: d3d11={} dxgi={} d3d10={} d3d12={} opengl32={} d3d9={}", - module_loaded("d3d11.dll"), - module_loaded("dxgi.dll"), - module_loaded("d3d10.dll"), - module_loaded("d3d12.dll"), - module_loaded("opengl32.dll"), - module_loaded("d3d9.dll"), - )); -} - -unsafe fn create_dummy_window() -> HWND { - let class_name = wide("FluxerGameCaptureDummyWindow"); - let instance = GetModuleHandleW(null()); - let window_class = WNDCLASSW { - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(dummy_window_proc), - hInstance: instance, - lpszClassName: class_name.as_ptr(), - ..Default::default() - }; - let _ = RegisterClassW(&window_class); - CreateWindowExW( - 0, - class_name.as_ptr(), - class_name.as_ptr(), - WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, - CW_USEDEFAULT, - 2, - 2, - null_mut(), - null_mut(), - instance, - null(), - ) -} - -unsafe fn swap_chain_method_pointers() -> Option<(*const (), Option<*const ()>)> { - let hwnd = create_dummy_window(); - if hwnd.is_null() { - verbose_log("d3d11 install: create_dummy_window FAILED"); - 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: 1, - OutputWindow: WinHwnd(hwnd), - Windowed: WinBool(1), - SwapEffect: DXGI_SWAP_EFFECT_DISCARD, - Flags: 0, - }; - let mut swap_chain = None; - let result = D3D11CreateDeviceAndSwapChain( - None, - D3D_DRIVER_TYPE_HARDWARE, - WinHmodule(null_mut()), - D3D11_CREATE_DEVICE_FLAG(0), - None, - D3D11_SDK_VERSION, - Some(&desc), - Some(&mut swap_chain), - None, - None, - None, - ); - let _ = DestroyWindow(hwnd); - if let Err(error) = result { - verbose_log(&format!( - "d3d11 install: D3D11CreateDeviceAndSwapChain dummy probe FAILED hr={:#010x}", - error.code().0 as u32 - )); - return None; - } - let Some(swap_chain) = swap_chain else { - verbose_log("d3d11 install: dummy swapchain was null despite Ok HRESULT"); - return None; - }; - let raw = swap_chain.as_raw(); - let vtable = *(raw as *mut *const *const ()); - let present = *vtable.add(8); - let present1 = swap_chain - .cast::() - .ok() - .map(|swap_chain1| { - let raw = swap_chain1.as_raw(); - let vtable = *(raw as *mut *const *const ()); - *vtable.add(22) - }); - verbose_log(&format!( - "d3d11 install: dummy swapchain created, Present vtable[8]={present:p} present1_present={}", - present1.is_some() - )); - Some((present, present1)) -} - -unsafe fn d3d12_execute_command_lists_pointer() -> Option<*const ()> { - let module_name = wide("d3d12.dll"); - let d3d12 = GetModuleHandleW(module_name.as_ptr()); - if d3d12.is_null() { - return None; - } - let create = GetProcAddress(d3d12, c"D3D12CreateDevice".as_ptr().cast())?; - let create: D3d12CreateDeviceFn = mem::transmute(create); - let mut device = null_mut(); - if create( - null_mut(), - D3D_FEATURE_LEVEL_11_0, - &ID3D12Device::IID, - &mut device, - ) - .is_err() - { - return None; - } - let device = ID3D12Device::from_raw(device); - let queue_desc = D3D12_COMMAND_QUEUE_DESC { - Type: D3D12_COMMAND_LIST_TYPE_DIRECT, - ..Default::default() - }; - let queue: ID3D12CommandQueue = device.CreateCommandQueue(&queue_desc).ok()?; - let raw = queue.as_raw(); - let vtable = *(raw as *mut *const *const ()); - Some(*vtable.add(10)) -} - -unsafe fn d3d11_hooks_installed() -> bool { - DXGI_PRESENT_HOOK.is_some() -} - -unsafe fn install_d3d11_hooks() { - if DXGI_PRESENT_HOOK.is_some() { - return; - } - let Some((present, present1)) = swap_chain_method_pointers() else { - return; - }; - if DXGI_PRESENT_HOOK.is_none() { - let target: DxgiPresentFn = mem::transmute(present); - let detour: DxgiPresentFn = dxgi_present_detour; - match Detour::::new(target, detour) { - Ok(hook) => match hook.enable() { - Ok(()) => { - DXGI_PRESENT_HOOK = Some(hook); - verbose_log("d3d11 install: IDXGISwapChain::Present detour ENABLED"); - } - Err(()) => verbose_log("d3d11 install: Present Detour.enable() FAILED"), - }, - Err(()) => verbose_log("d3d11 install: Present Detour::new() FAILED"), - } - } - if DXGI_PRESENT1_HOOK.is_none() { - if let Some(present1) = present1 { - let target: DxgiPresent1Fn = mem::transmute(present1); - let detour: DxgiPresent1Fn = dxgi_present1_detour; - match Detour::::new(target, detour) { - Ok(hook) => match hook.enable() { - Ok(()) => { - DXGI_PRESENT1_HOOK = Some(hook); - verbose_log("d3d11 install: IDXGISwapChain1::Present1 detour ENABLED"); - } - Err(()) => verbose_log("d3d11 install: Present1 Detour.enable() FAILED"), - }, - Err(()) => verbose_log("d3d11 install: Present1 Detour::new() FAILED"), - } - } - } -} - -unsafe fn install_d3d12_hooks() { - if env_flag_enabled(ENV_DISABLE_D3D12) { - return; - } - let Some(execute_command_lists) = d3d12_execute_command_lists_pointer() else { - return; - }; - if D3D12_EXECUTE_COMMAND_LISTS_HOOK.is_none() { - let target: D3d12ExecuteCommandListsFn = mem::transmute(execute_command_lists); - let detour: D3d12ExecuteCommandListsFn = d3d12_execute_command_lists_detour; - match Detour::::new(target, detour) { - Ok(hook) => match hook.enable() { - Ok(()) => { - D3D12_EXECUTE_COMMAND_LISTS_HOOK = Some(hook); - verbose_log("d3d12 install: ExecuteCommandLists detour ENABLED"); - } - Err(()) => verbose_log("d3d12 install: ExecuteCommandLists Detour.enable() FAILED"), - }, - Err(()) => verbose_log("d3d12 install: ExecuteCommandLists Detour::new() FAILED"), - } - } -} - -unsafe fn gl_hooks_installed() -> bool { - SWAP_BUFFERS_HOOK.is_some() && WGL_SWAP_LAYER_BUFFERS_HOOK.is_some() -} - -unsafe fn exported_swap_buffers() -> Option { - let module_name = wide("gdi32.dll"); - let module = GetModuleHandleW(module_name.as_ptr()); - if module.is_null() { - verbose_log("opengl install: gdi32.dll not loaded; cannot resolve SwapBuffers"); - return None; - } - let proc = GetProcAddress(module, c"SwapBuffers".as_ptr().cast())?; - Some(mem::transmute(proc)) -} - -unsafe fn exported_wgl_swap_layer_buffers() -> Option { - let module_name = wide("opengl32.dll"); - let module = GetModuleHandleW(module_name.as_ptr()); - if module.is_null() { - verbose_log("opengl install: opengl32.dll not loaded; cannot resolve wglSwapLayerBuffers"); - return None; - } - let proc = GetProcAddress(module, c"wglSwapLayerBuffers".as_ptr().cast())?; - Some(mem::transmute(proc)) -} - -unsafe fn prologue_hex(function: *const ()) -> String { - let ptr = function as *const u8; - if ptr.is_null() { - return "".to_string(); - } - let bytes = std::slice::from_raw_parts(ptr, arm64_reloc::STOLEN_BYTES); - bytes - .iter() - .map(|byte| format!("{byte:02X}")) - .collect::>() - .join(" ") -} - -unsafe fn install_opengl_hooks() { - if SWAP_BUFFERS_HOOK.is_none() { - if let Some(target) = exported_swap_buffers() { - verbose_log(&format!( - "opengl install: SwapBuffers target prologue {}", - prologue_hex(target as *const ()) - )); - let detour: SwapBuffersFn = swap_buffers_detour; - match Detour::::new(target, detour) { - Ok(hook) => match hook.enable() { - Ok(()) => { - SWAP_BUFFERS_HOOK = Some(hook); - verbose_log("opengl install: SwapBuffers detour ENABLED"); - } - Err(()) => verbose_log("opengl install: SwapBuffers Detour.enable() FAILED"), - }, - Err(()) => verbose_log("opengl install: SwapBuffers Detour::new() FAILED"), - } - } - } - if WGL_SWAP_LAYER_BUFFERS_HOOK.is_none() { - if let Some(target) = exported_wgl_swap_layer_buffers() { - let detour: WglSwapLayerBuffersFn = wgl_swap_layer_buffers_detour; - match Detour::::new(target, detour) { - Ok(hook) => match hook.enable() { - Ok(()) => { - WGL_SWAP_LAYER_BUFFERS_HOOK = Some(hook); - verbose_log("opengl install: wglSwapLayerBuffers detour ENABLED"); - } - Err(()) => { - verbose_log("opengl install: wglSwapLayerBuffers Detour.enable() FAILED") - } - }, - Err(()) => verbose_log("opengl install: wglSwapLayerBuffers Detour::new() FAILED"), - } - } - } -} - -unsafe fn install_hooks() { - install_opengl_hooks(); - install_d3d11_hooks(); - install_d3d12_hooks(); - d3d9::install_d3d9_hooks(); - HOOKS_READY.store(true, Ordering::Release); -} - -unsafe extern "system" fn hook_thread(_param: *mut c_void) -> u32 { - verbose_log("hook_thread started -- installing graphics detours"); - log_loaded_graphics_modules("hook_thread initial"); - install_hooks(); - let want_d3d12 = !env_flag_enabled(ENV_DISABLE_D3D12); - for tick in 0..50 { - let d3d11_ok = d3d11_hooks_installed(); - let gl_ok = gl_hooks_installed() || module_loaded("opengl32.dll"); - let d3d12_ok = !want_d3d12 || D3D12_EXECUTE_COMMAND_LISTS_HOOK.is_some(); - if d3d11_ok && d3d12_ok && gl_ok && d3d9::hooks_installed() { - verbose_log(&format!( - "hook_thread: all targeted detours installed after {tick} retries" - )); - break; - } - Sleep(200); - log_loaded_graphics_modules("hook_thread retry"); - install_opengl_hooks(); - install_d3d11_hooks(); - if want_d3d12 { - install_d3d12_hooks(); - } - d3d9::install_d3d9_hooks(); - } - verbose_log(&format!( - "hook_thread: install poll finished (dxgi_present={} dxgi_present1={} d3d12={} opengl={} d3d9={})", - DXGI_PRESENT_HOOK.is_some(), - DXGI_PRESENT1_HOOK.is_some(), - D3D12_EXECUTE_COMMAND_LISTS_HOOK.is_some(), - gl_hooks_installed(), - d3d9::hooks_installed(), - )); - 0 -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn FluxerGetMsgProc( - code: i32, - wparam: WPARAM, - lparam: WinLparam, -) -> LRESULT { - CallNextHookEx(null_mut(), code, wparam, lparam) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn DllMain( - hinst: HINSTANCE, - reason: u32, - _reserved: *mut c_void, -) -> BOOL { - if reason == DLL_PROCESS_ATTACH { - verbose_log(&format!( - "DllMain DLL_PROCESS_ATTACH reached (pointer_width={} bits)", - mem::size_of::() * 8 - )); - DisableThreadLibraryCalls(hinst); - let thread = CreateThread(null(), 0, Some(hook_thread), null(), 0, null_mut()); - if thread.is_null() { - verbose_log("DllMain CreateThread(hook_thread) FAILED -- no hooks will install"); - } else { - CloseHandle(thread); - } - } - 1 -} diff --git a/fluxer_desktop/native/win-game-capture/hook/tests/arm64_reloc.rs b/fluxer_desktop/native/win-game-capture/hook/tests/arm64_reloc.rs deleted file mode 100644 index de69ae4e2..000000000 --- a/fluxer_desktop/native/win-game-capture/hook/tests/arm64_reloc.rs +++ /dev/null @@ -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); -} diff --git a/fluxer_desktop/native/win-game-capture/index.d.ts b/fluxer_desktop/native/win-game-capture/index.d.ts index 1f74e8876..ecdd80a18 100644 --- a/fluxer_desktop/native/win-game-capture/index.d.ts +++ b/fluxer_desktop/native/win-game-capture/index.d.ts @@ -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; stop(): Promise; 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>; -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; diff --git a/fluxer_desktop/native/win-game-capture/index.js b/fluxer_desktop/native/win-game-capture/index.js index 6df6c6b91..a76fc285a 100644 --- a/fluxer_desktop/native/win-game-capture/index.js +++ b/fluxer_desktop/native/win-game-capture/index.js @@ -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, diff --git a/fluxer_desktop/native/win-game-capture/index.test.mjs b/fluxer_desktop/native/win-game-capture/index.test.mjs index b037eb43c..f9d165189 100644 --- a/fluxer_desktop/native/win-game-capture/index.test.mjs +++ b/fluxer_desktop/native/win-game-capture/index.test.mjs @@ -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', ); diff --git a/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.lock b/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.lock deleted file mode 100644 index 3b8e8173f..000000000 --- a/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.toml b/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.toml deleted file mode 100644 index 7e53060eb..000000000 --- a/fluxer_desktop/native/win-game-capture/inject-helper/Cargo.toml +++ /dev/null @@ -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", -]} diff --git a/fluxer_desktop/native/win-game-capture/inject-helper/src/main.rs b/fluxer_desktop/native/win-game-capture/inject-helper/src/main.rs deleted file mode 100644 index c23442e0c..000000000 --- a/fluxer_desktop/native/win-game-capture/inject-helper/src/main.rs +++ /dev/null @@ -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 { - 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 = std::env::args().skip(1).collect(); - if args.len() < 2 || args.len() > 3 { - log(&format!( - "bad args: expected [timeout-ms], got {} arg(s)", - args.len() - )); - return Stage::BadArgs; - } - let Ok(target_pid) = args[0].parse::() 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::() { - 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::(); - - 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 - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/src/compatibility.rs b/fluxer_desktop/native/win-game-capture/src/compatibility.rs deleted file mode 100644 index a4bcf9867..000000000 --- a/fluxer_desktop/native/win-game-capture/src/compatibility.rs +++ /dev/null @@ -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, -) -> 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 { - 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 { - 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 { - 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, - allow: Vec, - force_cpu: Vec, -} - -impl OverrideLists { - fn is_empty(&self) -> bool { - self.deny.is_empty() && self.allow.is_empty() && self.force_cpu.is_empty() - } -} - -fn load_override() -> Option { - 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 { - 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 { - 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 { - None -} - -#[cfg(target_os = "windows")] -fn target_process_exe_name(pid: u32) -> Option { - 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 { - 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 { - 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 { - 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 { - 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"); - } -} diff --git a/fluxer_desktop/native/win-game-capture/src/dxgi_capture.rs b/fluxer_desktop/native/win-game-capture/src/dxgi_capture.rs index 1b762c3c8..6d1af9513 100644 --- a/fluxer_desktop/native/win-game-capture/src/dxgi_capture.rs +++ b/fluxer_desktop/native/win-game-capture/src/dxgi_capture.rs @@ -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(); diff --git a/fluxer_desktop/native/win-game-capture/src/game_capture.rs b/fluxer_desktop/native/win-game-capture/src/game_capture.rs deleted file mode 100644 index 9f93235df..000000000 --- a/fluxer_desktop/native/win-game-capture/src/game_capture.rs +++ /dev/null @@ -1,1521 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::fallback; -use crate::game_capture_abi::{ENV_FORCE_CPU, ENV_VERBOSE, env_flag_enabled}; -use crate::nv12_gpu::Nv12GpuConverter; -use crate::{ - CaptureInner, - capture_target::{monitor_rect, resolve_game_capture_target}, - compatibility::{InjectionPolicy, injection_policy_for_window}, - d3d11_device::create_shared_texture_device, - dxgi_capture::{resolve_output_size, wall_clock_us}, - emit_lifecycle, emit_shared_texture_frame, - game_capture_abi::{ - GAME_CAPTURE_API_OPENGL, GAME_CAPTURE_BUFFER_COUNT, - GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE, GAME_CAPTURE_FRAME_PREFIX, - GAME_CAPTURE_INFO_PREFIX, GAME_CAPTURE_KEEPALIVE_PREFIX, GAME_CAPTURE_READY_PREFIX, - GAME_CAPTURE_STATE_ERROR, GAME_CAPTURE_STATE_RESIZE_REQUIRED, GAME_CAPTURE_STATE_STOPPED, - GAME_CAPTURE_STOP_PREFIX, GAME_CAPTURE_TRANSPORT_MEMORY, - GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE, GameCaptureSharedInfo, mutex_name, object_name, - presented_recently, qpc_now_us, shared_frame_mapping_size, - }, - note_cpu_fallback_frame_dropped, note_media_frame_without_sink, -}; -use std::{ - ffi::c_void, - path::Path, - ptr::{null, null_mut}, - sync::{Arc, Mutex, atomic::Ordering}, -}; -use windows::Win32::{ - Foundation::HANDLE as WinHandle, - Graphics::{ - Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D}, - Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIFactory1}, - }, -}; -use windows_sys::Win32::{ - Foundation::{ - CloseHandle, FreeLibrary, HANDLE, HINSTANCE, HMODULE as WinSysHmodule, HWND, - INVALID_HANDLE_VALUE, LPARAM, RECT, WAIT_ABANDONED, WAIT_OBJECT_0, WAIT_TIMEOUT, - }, - Graphics::Gdi::{MONITOR_DEFAULTTONEAREST, MonitorFromRect}, - System::{ - Diagnostics::Debug::WriteProcessMemory, - LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW}, - Memory::{ - CreateFileMappingW, FILE_MAP_ALL_ACCESS, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, - MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, PAGE_READWRITE, UnmapViewOfFile, - VirtualAllocEx, VirtualFreeEx, - }, - SystemInformation::{ - IMAGE_FILE_MACHINE, IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM, - IMAGE_FILE_MACHINE_ARM64, IMAGE_FILE_MACHINE_ARMNT, IMAGE_FILE_MACHINE_I386, - IMAGE_FILE_MACHINE_IA64, IMAGE_FILE_MACHINE_UNKNOWN, - }, - Threading::{ - CreateEventW, CreateMutexW, CreateRemoteThread, GetCurrentProcessId, IsWow64Process, - IsWow64Process2, OpenProcess, PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, - PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, ResetEvent, - SetEvent, WaitForSingleObject, - }, - }, - UI::WindowsAndMessaging::{ - GetClientRect, GetWindowRect, GetWindowThreadProcessId, HHOOK, IsWindow, IsWindowVisible, - PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_GETMESSAGE, WM_NULL, - }, -}; - -const MAX_GAME_CAPTURE_DIMENSION: u32 = 8192; -const MAX_GAME_CAPTURE_FRAME_BYTES: usize = 384 * 1024 * 1024; -const FRAME_EVENT_WAIT_MS: u32 = 100; - -const HOOK_PROC_EXPORT: &[u8] = b"FluxerGetMsgProc\0"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum InjectionMethod { - Auto, - RemoteThread, - SetWindowsHook, -} - -impl InjectionMethod { - fn resolve(explicit: Option<&str>) -> Self { - if let Some(explicit) = explicit { - return Self::parse(explicit).unwrap_or(Self::Auto); - } - if let Some(value) = std::env::var_os(crate::game_capture_abi::ENV_INJECT_METHOD) { - return Self::parse(&value.to_string_lossy()).unwrap_or(Self::Auto); - } - Self::Auto - } - - fn parse(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "auto" => Some(Self::Auto), - "remote-thread" => Some(Self::RemoteThread), - "set-windows-hook" => Some(Self::SetWindowsHook), - _ => None, - } - } - - fn as_str(self) -> &'static str { - match self { - Self::Auto => "auto", - Self::RemoteThread => "remote-thread", - Self::SetWindowsHook => "set-windows-hook", - } - } -} - -struct InstalledWindowsHook { - hook: HHOOK, - module: WinSysHmodule, -} - -unsafe impl Send for InstalledWindowsHook {} -unsafe impl Sync for InstalledWindowsHook {} - -impl Drop for InstalledWindowsHook { - fn drop(&mut self) { - unsafe { - if !self.hook.is_null() { - UnhookWindowsHookEx(self.hook); - self.hook = null_mut(); - } - if !self.module.is_null() { - FreeLibrary(self.module); - self.module = null_mut(); - } - } - } -} - -const STALL_THRESHOLD: std::time::Duration = std::time::Duration::from_millis(1000); -const RECENT_PRESENT_WINDOW_US: i64 = 1_000_000; - -#[derive(Debug)] -struct OwnedHandle(HANDLE); - -impl OwnedHandle { - fn new(handle: HANDLE, context: &str) -> Result { - if handle.is_null() || handle == INVALID_HANDLE_VALUE { - return Err(format!("{context}: failed to create/open handle")); - } - Ok(Self(handle)) - } - - fn raw(&self) -> HANDLE { - self.0 - } -} - -unsafe impl Send for OwnedHandle {} -unsafe impl Sync for OwnedHandle {} - -impl Drop for OwnedHandle { - fn drop(&mut self) { - if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE { - unsafe { - CloseHandle(self.0); - } - } - self.0 = null_mut(); - } -} - -#[derive(Debug)] -struct MappedView { - _mapping: OwnedHandle, - ptr: *mut T, -} - -unsafe impl Send for MappedView {} -unsafe impl Sync for MappedView {} - -impl MappedView { - fn create(name: &str, size: usize) -> Result { - if size == 0 || size > u32::MAX as usize { - return Err(format!("invalid mapping size for {name}: {size}")); - } - let wide_name = to_wide(name); - let mapping = unsafe { - CreateFileMappingW( - INVALID_HANDLE_VALUE, - null(), - PAGE_READWRITE, - 0, - size as u32, - wide_name.as_ptr(), - ) - }; - let mapping = OwnedHandle::new(mapping, "CreateFileMappingW")?; - let view = unsafe { MapViewOfFile(mapping.raw(), FILE_MAP_ALL_ACCESS, 0, 0, size) }; - if view.Value.is_null() { - return Err(format!("MapViewOfFile failed for {name}")); - } - Ok(Self { - _mapping: mapping, - ptr: view.Value.cast(), - }) - } -} - -impl Drop for MappedView { - fn drop(&mut self) { - if !self.ptr.is_null() { - unsafe { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: self.ptr.cast(), - }); - } - } - self.ptr = null_mut(); - } -} - -pub struct GameCaptureSession { - target_hwnd: HWND, - capture_width: u32, - capture_height: u32, - output_width: u32, - output_height: u32, - info: MappedView, - frames: MappedView, - frame_buffer_capacity: usize, - ready_event: OwnedHandle, - stop_event: OwnedHandle, - _keepalive_mutex: OwnedHandle, - frame_mutexes: [OwnedHandle; GAME_CAPTURE_BUFFER_COUNT], - force_cpu_readback: bool, - requested_injection_method: InjectionMethod, - used_injection_method: InjectionMethod, - native_texture: Mutex>, - _windows_hook: Option, -} - -unsafe impl Send for GameCaptureSession {} -unsafe impl Sync for GameCaptureSession {} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct NativeTextureHandleInfo { - pub handle: u64, - pub width: u32, - pub height: u32, - pub dxgi_format: u32, - pub timestamp_us: i64, -} - -struct SharedTextureReader { - device: ID3D11Device, - context: ID3D11DeviceContext, - handle: u64, - width: u32, - height: u32, - format: u32, - source_format: crate::hdr::SourceFormat, - nv12: Option, -} - -impl Drop for GameCaptureSession { - fn drop(&mut self) { - unsafe { - SetEvent(self.stop_event.raw()); - } - } -} - -impl GameCaptureSession { - #[allow(clippy::too_many_arguments)] - pub fn new( - source_id: &str, - source_kind: &str, - requested_width: Option, - requested_height: Option, - frame_rate: u32, - hook_path: &str, - hook_path_x86: Option<&str>, - injection_method: Option<&str>, - ) -> Result { - let requested_injection_method = InjectionMethod::resolve(injection_method); - let target_hwnd = resolve_game_capture_target(source_id, source_kind)?; - let target_pid = target_process_id(target_hwnd)?; - if target_pid == unsafe { GetCurrentProcessId() } { - return Err("refusing to inject game capture hook into Fluxer".into()); - } - - let force_cpu_readback = match injection_policy_for_window(target_pid, target_hwnd) { - InjectionPolicy::Deny(reason) => return Err(reason), - InjectionPolicy::ForceCpuReadback => true, - InjectionPolicy::Allow => false, - }; - - let target_machine = target_process_machine(target_pid)?; - let target_is_32_bit = machine_is_32_bit(target_machine)?; - let selected_hook_path = if target_is_32_bit { - hook_path_x86.ok_or( - "target game process is 32-bit but no 32-bit game capture hook DLL was provided", - )? - } else if target_machine == HOST_MACHINE { - hook_path - } else { - return Err(format!( - "target game process architecture (IMAGE_FILE_MACHINE 0x{target_machine:04x}) \ - differs from the host architecture and no matching game capture hook DLL ships for it" - )); - }; - - let (capture_width, capture_height) = window_capture_size(target_hwnd)?; - validate_capture_size(capture_width, capture_height)?; - let (output_width, output_height) = resolve_output_size( - capture_width, - capture_height, - requested_width, - requested_height, - ); - let (buffer_width, buffer_height) = - shared_buffer_dimensions_for_window(target_hwnd, capture_width, capture_height); - let frame_map_size = shared_frame_mapping_size(buffer_width, buffer_height) - .ok_or("game capture frame mapping size overflow")?; - if frame_map_size > MAX_GAME_CAPTURE_FRAME_BYTES { - return Err(format!( - "game capture frame mapping too large: {frame_map_size} bytes" - )); - } - - let info_name = object_name(GAME_CAPTURE_INFO_PREFIX, target_pid); - let frame_name = object_name(GAME_CAPTURE_FRAME_PREFIX, target_pid); - let ready_name = object_name(GAME_CAPTURE_READY_PREFIX, target_pid); - let stop_name = object_name(GAME_CAPTURE_STOP_PREFIX, target_pid); - let keepalive_name = object_name(GAME_CAPTURE_KEEPALIVE_PREFIX, target_pid); - let mutex_names = [mutex_name(target_pid, 0), mutex_name(target_pid, 1)]; - - let info = MappedView::::create( - &info_name, - std::mem::size_of::(), - )?; - unsafe { - std::ptr::write_volatile( - info.ptr, - GameCaptureSharedInfo::new( - target_hwnd as usize as u64, - buffer_width, - buffer_height, - frame_rate, - ), - ); - } - let frames = MappedView::::create(&frame_name, frame_map_size)?; - let ready_event = create_event(&ready_name, false, false)?; - let stop_event = create_event(&stop_name, true, false)?; - let keepalive_mutex = create_mutex(&keepalive_name)?; - let frame_mutexes = [ - create_mutex(&mutex_names[0])?, - create_mutex(&mutex_names[1])?, - ]; - unsafe { - ResetEvent(ready_event.raw()); - ResetEvent(stop_event.raw()); - } - let Injected { - method: used_injection_method, - windows_hook, - } = inject( - requested_injection_method, - target_hwnd, - target_pid, - target_is_32_bit, - selected_hook_path, - )?; - - Ok(Self { - target_hwnd, - capture_width: buffer_width, - capture_height: buffer_height, - output_width, - output_height, - info, - frames, - frame_buffer_capacity: frame_map_size / GAME_CAPTURE_BUFFER_COUNT, - ready_event, - stop_event, - _keepalive_mutex: keepalive_mutex, - frame_mutexes, - force_cpu_readback, - requested_injection_method, - used_injection_method, - native_texture: Mutex::new(None), - _windows_hook: windows_hook, - }) - } - - #[allow(clippy::misnamed_getters)] - pub fn capture_width(&self) -> u32 { - self.output_width - } - - #[allow(clippy::misnamed_getters)] - pub fn capture_height(&self) -> u32 { - self.output_height - } - - pub fn force_cpu_readback(&self) -> bool { - self.force_cpu_readback - } - - pub fn requested_injection_method(&self) -> &'static str { - self.requested_injection_method.as_str() - } - - pub fn used_injection_method(&self) -> &'static str { - self.used_injection_method.as_str() - } - - pub fn read_shared_info(&self) -> Option { - let info = unsafe { std::ptr::read_volatile(self.info.ptr) }; - if info.magic == crate::game_capture_abi::GAME_CAPTURE_MAGIC { - Some(info) - } else { - None - } - } - - pub fn read_native_texture_info(&self) -> Option { - self.native_texture.lock().ok().and_then(|guard| *guard) - } - - fn set_native_texture_info(&self, info: Option) { - if let Ok(mut guard) = self.native_texture.lock() { - *guard = info; - } - } - - fn request_hook_disable_shared_texture(&self) { - unsafe { - let control = &mut (*self.info.ptr).control; - std::ptr::write_volatile( - control, - std::ptr::read_volatile(control) | GAME_CAPTURE_CONTROL_DISABLE_SHARED_TEXTURE, - ); - } - } -} - -fn to_wide(value: &str) -> Vec { - value.encode_utf16().chain(std::iter::once(0)).collect() -} - -fn create_event( - name: &str, - manual_reset: bool, - initial_state: bool, -) -> Result { - let wide_name = to_wide(name); - let handle = unsafe { - CreateEventW( - null(), - i32::from(manual_reset), - i32::from(initial_state), - wide_name.as_ptr(), - ) - }; - OwnedHandle::new(handle, "CreateEventW") -} - -fn create_mutex(name: &str) -> Result { - let wide_name = to_wide(name); - let handle = unsafe { CreateMutexW(null(), 0, wide_name.as_ptr()) }; - OwnedHandle::new(handle, "CreateMutexW") -} - -fn verbose_log(message: &str) { - if !env_flag_enabled(ENV_VERBOSE) { - return; - } - let text = format!("[fluxer-game-capture] {message}"); - let line = to_wide(&text); - unsafe { - windows_sys::Win32::System::Diagnostics::Debug::OutputDebugStringW(line.as_ptr()); - } - use std::io::Write; - let _ = writeln!(std::io::stderr(), "{text}"); -} - -struct Injected { - method: InjectionMethod, - windows_hook: Option, -} - -const HOST_IS_32_BIT: bool = cfg!(target_pointer_width = "32"); -const HOST_MACHINE: IMAGE_FILE_MACHINE = if cfg!(target_arch = "x86_64") { - IMAGE_FILE_MACHINE_AMD64 -} else if cfg!(target_arch = "aarch64") { - IMAGE_FILE_MACHINE_ARM64 -} else if cfg!(target_arch = "x86") { - IMAGE_FILE_MACHINE_I386 -} else { - IMAGE_FILE_MACHINE_UNKNOWN -}; - -fn inject( - method: InjectionMethod, - target_hwnd: HWND, - target_pid: u32, - target_is_32_bit: bool, - hook_path: &str, -) -> Result { - let set_windows_hook_possible = target_is_32_bit == HOST_IS_32_BIT; - verbose_log(&format!( - "injecting hook (method={}, target_pid={target_pid}, target_32bit={target_is_32_bit}, \ - set_windows_hook_possible={set_windows_hook_possible})", - method.as_str() - )); - - match method { - InjectionMethod::RemoteThread => { - inject_via_remote_thread(target_pid, hook_path, target_is_32_bit)?; - Ok(Injected { - method: InjectionMethod::RemoteThread, - windows_hook: None, - }) - } - InjectionMethod::SetWindowsHook => { - if !set_windows_hook_possible { - return Err( - "the SetWindowsHookEx injection method requires the target game to match \ - Fluxer's bitness; this target is a different bitness, so pick the \ - remote-thread or automatic method instead" - .into(), - ); - } - let windows_hook = inject_via_set_windows_hook(target_hwnd, target_pid, hook_path)?; - Ok(Injected { - method: InjectionMethod::SetWindowsHook, - windows_hook: Some(windows_hook), - }) - } - InjectionMethod::Auto => { - match inject_via_remote_thread(target_pid, hook_path, target_is_32_bit) { - Ok(()) => Ok(Injected { - method: InjectionMethod::RemoteThread, - windows_hook: None, - }), - Err(remote_thread_error) => { - if !set_windows_hook_possible { - return Err(remote_thread_error); - } - verbose_log(&format!( - "remote-thread injection failed ({remote_thread_error}); falling back to \ - SetWindowsHookEx" - )); - match inject_via_set_windows_hook(target_hwnd, target_pid, hook_path) { - Ok(windows_hook) => Ok(Injected { - method: InjectionMethod::SetWindowsHook, - windows_hook: Some(windows_hook), - }), - Err(set_hook_error) => Err(format!( - "game capture injection failed: remote-thread method failed \ - ({remote_thread_error}); SetWindowsHookEx fallback also failed \ - ({set_hook_error})" - )), - } - } - } - } - } -} - -fn inject_via_remote_thread( - target_pid: u32, - hook_path: &str, - target_is_32_bit: bool, -) -> Result<(), String> { - let hook_path_buf = Path::new(hook_path); - if !hook_path_buf.exists() { - return Err(format!( - "game capture hook DLL missing: {}", - hook_path_buf.display() - )); - } - - if target_is_32_bit != HOST_IS_32_BIT { - return inject_via_helper(target_pid, hook_path); - } - - let hook_path = to_wide(&hook_path_buf.to_string_lossy()); - let hook_path_bytes = hook_path - .len() - .checked_mul(std::mem::size_of::()) - .ok_or("hook path size overflow")?; - - unsafe { - let process = OpenProcess( - PROCESS_CREATE_THREAD - | PROCESS_QUERY_INFORMATION - | PROCESS_VM_OPERATION - | PROCESS_VM_WRITE, - 0, - target_pid, - ); - let process = OwnedHandle::new(process, "OpenProcess")?; - let remote_path = VirtualAllocEx( - process.raw(), - null(), - hook_path_bytes, - MEM_COMMIT | MEM_RESERVE, - PAGE_READWRITE, - ); - if remote_path.is_null() { - return Err("VirtualAllocEx failed while injecting game capture hook".into()); - } - - let mut written = 0usize; - let write_ok = WriteProcessMemory( - process.raw(), - remote_path, - hook_path.as_ptr().cast(), - hook_path_bytes, - &mut written, - ) != 0; - if !write_ok || written != hook_path_bytes { - VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE); - return Err("WriteProcessMemory failed while injecting game capture hook".into()); - } - - let kernel32_name = to_wide("kernel32.dll"); - let kernel32 = GetModuleHandleW(kernel32_name.as_ptr()); - if kernel32.is_null() { - VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE); - return Err( - "GetModuleHandleW(kernel32.dll) failed while injecting game capture hook".into(), - ); - } - let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr().cast()); - let Some(load_library) = load_library else { - VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE); - return Err( - "GetProcAddress(LoadLibraryW) failed while injecting game capture hook".into(), - ); - }; - 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(), - ); - let thread = match OwnedHandle::new(thread, "CreateRemoteThread") { - Ok(thread) => thread, - Err(error) => { - VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE); - return Err(error); - } - }; - let wait = WaitForSingleObject(thread.raw(), 5000); - VirtualFreeEx(process.raw(), remote_path, 0, MEM_RELEASE); - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - return Err("game capture hook injection timed out".into()); - } - } - Ok(()) -} - -fn helper_stage_reason(code: i32) -> &'static str { - match code { - 2 => "bad helper arguments", - 3 => "hook DLL missing (as seen by the helper)", - 4 => "OpenProcess failed in the helper (target gone or insufficient rights)", - 5 => "VirtualAllocEx failed in the helper", - 6 => "WriteProcessMemory failed in the helper", - 7 => "GetModuleHandleW(kernel32) failed in the helper", - 8 => "GetProcAddress(LoadLibraryW) failed in the helper", - 9 => "CreateRemoteThread failed in the helper", - 10 => "the remote LoadLibraryW thread timed out", - 11 => "LoadLibraryW returned NULL in the target (the hook DLL failed to load)", - 64 => "the inject-helper exe is not a Windows build", - _ => "unknown helper failure", - } -} - -fn helper_path_for_hook(hook_path: &str) -> Result { - let hook = Path::new(hook_path); - let dir = hook - .parent() - .ok_or("could not resolve the hook DLL directory for the inject-helper")?; - let file_name = hook - .file_name() - .and_then(|name| name.to_str()) - .ok_or("could not resolve the hook DLL file name for the inject-helper")?; - let tag = file_name - .strip_prefix("fluxer-game-hook.") - .and_then(|rest| rest.strip_suffix(".dll")) - .ok_or_else(|| { - format!("unexpected hook DLL file name (cannot derive inject-helper): {file_name}") - })?; - Ok(dir.join(format!("fluxer-inject-helper.{tag}.exe"))) -} - -fn inject_via_helper(target_pid: u32, hook_path: &str) -> Result<(), String> { - let helper = helper_path_for_hook(hook_path)?; - if !helper.exists() { - return Err(format!( - "cross-bitness game capture needs the matching-bitness inject-helper, but it is \ - missing: {}", - helper.display() - )); - } - - verbose_log(&format!( - "spawning cross-bitness inject-helper {} (target_pid={target_pid})", - helper.display() - )); - - let status = std::process::Command::new(&helper) - .arg(target_pid.to_string()) - .arg(hook_path) - .status() - .map_err(|error| { - format!( - "failed to launch the cross-bitness inject-helper ({}): {error}", - helper.display() - ) - })?; - - match status.code() { - Some(0) => Ok(()), - Some(code) => Err(format!( - "cross-bitness inject-helper failed (exit {code}: {})", - helper_stage_reason(code) - )), - None => Err(format!( - "cross-bitness inject-helper terminated without an exit code ({})", - helper.display() - )), - } -} - -fn inject_via_set_windows_hook( - target_hwnd: HWND, - target_pid: u32, - hook_path: &str, -) -> Result { - let hook_path = Path::new(hook_path); - if !hook_path.exists() { - return Err(format!( - "game capture hook DLL missing: {}", - hook_path.display() - )); - } - - let mut resolved_pid = 0u32; - let thread_id = unsafe { GetWindowThreadProcessId(target_hwnd, &mut resolved_pid) }; - if thread_id == 0 { - return Err("failed to resolve target window thread for SetWindowsHookEx injection".into()); - } - if resolved_pid != target_pid { - return Err( - "target window thread no longer belongs to the expected process; aborting \ - SetWindowsHookEx injection" - .into(), - ); - } - - let wide_path = to_wide(&hook_path.to_string_lossy()); - let module = unsafe { LoadLibraryW(wide_path.as_ptr()) }; - if module.is_null() { - return Err( - "LoadLibraryW failed to load the game capture hook DLL for SetWindowsHookEx injection" - .into(), - ); - } - - let proc = unsafe { GetProcAddress(module, HOOK_PROC_EXPORT.as_ptr()) }; - let Some(proc) = proc else { - unsafe { - FreeLibrary(module); - } - return Err( - "GetProcAddress(FluxerGetMsgProc) failed; the hook DLL is missing the SetWindowsHookEx \ - entry point" - .into(), - ); - }; - - type HookFn = - unsafe extern "system" fn(i32, windows_sys::Win32::Foundation::WPARAM, LPARAM) -> isize; - let hook_fn: HookFn = unsafe { std::mem::transmute(proc) }; - let hook = - unsafe { SetWindowsHookExW(WH_GETMESSAGE, Some(hook_fn), module as HINSTANCE, thread_id) }; - if hook.is_null() { - unsafe { - FreeLibrary(module); - } - return Err("SetWindowsHookExW failed while injecting the game capture hook".into()); - } - - unsafe { - PostThreadMessageW(thread_id, WM_NULL, 0, 0); - } - - Ok(InstalledWindowsHook { hook, module }) -} - -fn target_process_id(hwnd: HWND) -> Result { - let mut pid = 0u32; - unsafe { - GetWindowThreadProcessId(hwnd, &mut pid); - } - if pid == 0 { - Err("failed to resolve target process id".into()) - } else { - Ok(pid) - } -} - -fn target_process_machine(target_pid: u32) -> Result { - let process = unsafe { - OpenProcess( - PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_QUERY_INFORMATION, - 0, - target_pid, - ) - }; - let process = OwnedHandle::new(process, "OpenProcess(query bitness)")?; - - let mut process_machine: IMAGE_FILE_MACHINE = IMAGE_FILE_MACHINE_UNKNOWN; - let mut native_machine: IMAGE_FILE_MACHINE = IMAGE_FILE_MACHINE_UNKNOWN; - let ok = unsafe { IsWow64Process2(process.raw(), &mut process_machine, &mut native_machine) }; - if ok != 0 { - let effective_machine = if process_machine == IMAGE_FILE_MACHINE_UNKNOWN { - native_machine - } else { - process_machine - }; - return Ok(effective_machine); - } - - let mut is_wow64: windows_sys::core::BOOL = 0; - let ok = unsafe { IsWow64Process(process.raw(), &mut is_wow64) }; - if ok == 0 { - return Err("failed to query target process bitness".into()); - } - if is_wow64 != 0 { - return Ok(IMAGE_FILE_MACHINE_I386); - } - Ok(HOST_MACHINE) -} - -fn machine_is_32_bit(machine: IMAGE_FILE_MACHINE) -> Result { - match machine { - IMAGE_FILE_MACHINE_I386 | IMAGE_FILE_MACHINE_ARM | IMAGE_FILE_MACHINE_ARMNT => Ok(true), - IMAGE_FILE_MACHINE_AMD64 | IMAGE_FILE_MACHINE_ARM64 | IMAGE_FILE_MACHINE_IA64 => Ok(false), - _ => Err(format!( - "unsupported target process architecture (IMAGE_FILE_MACHINE 0x{machine:04x}); \ - cannot pick a game capture hook DLL for it" - )), - } -} - -fn window_capture_size(hwnd: HWND) -> Result<(u32, u32), String> { - let mut rect = RECT::default(); - if unsafe { GetClientRect(hwnd, &mut rect) } != 0 { - let width = (rect.right - rect.left).max(0) as u32; - let height = (rect.bottom - rect.top).max(0) as u32; - if width > 0 && height > 0 { - return Ok((width, height)); - } - } - if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 { - return Err("failed to resolve target window size".into()); - } - let width = (rect.right - rect.left).max(1) as u32; - let height = (rect.bottom - rect.top).max(1) as u32; - Ok((width, height)) -} - -fn validate_capture_size(width: u32, height: u32) -> Result<(), String> { - if width == 0 || height == 0 { - return Err("game capture target has zero size".into()); - } - if width > MAX_GAME_CAPTURE_DIMENSION || height > MAX_GAME_CAPTURE_DIMENSION { - return Err(format!("game capture target too large: {width}x{height}")); - } - Ok(()) -} - -fn rect_size(rect: &RECT) -> Option<(u32, u32)> { - let width = (rect.right as i64 - rect.left as i64).abs(); - let height = (rect.bottom as i64 - rect.top as i64).abs(); - if width <= 0 || height <= 0 { - return None; - } - Some((width as u32, height as u32)) -} - -fn shared_buffer_dimensions_for_window( - hwnd: HWND, - capture_width: u32, - capture_height: u32, -) -> (u32, u32) { - let mut window_rect = RECT::default(); - let monitor = if unsafe { GetWindowRect(hwnd, &mut window_rect) } != 0 { - unsafe { MonitorFromRect(&window_rect, MONITOR_DEFAULTTONEAREST) } - } else { - null_mut() - }; - let Some(monitor_rect) = (!monitor.is_null()) - .then(|| monitor_rect(monitor)) - .flatten() - else { - return (capture_width, capture_height); - }; - let Some((monitor_width, monitor_height)) = rect_size(&monitor_rect) else { - return (capture_width, capture_height); - }; - let Some((buffer_width, buffer_height)) = choose_shared_buffer_dimensions( - capture_width, - capture_height, - monitor_width, - monitor_height, - ) else { - verbose_log(&format!( - "nearest monitor buffer {monitor_width}x{monitor_height} exceeds capture mapping limit; \ - using initial window buffer {capture_width}x{capture_height}" - )); - return (capture_width, capture_height); - }; - (buffer_width, buffer_height) -} - -fn choose_shared_buffer_dimensions( - capture_width: u32, - capture_height: u32, - monitor_width: u32, - monitor_height: u32, -) -> Option<(u32, u32)> { - let buffer_width = capture_width - .max(monitor_width) - .min(MAX_GAME_CAPTURE_DIMENSION); - let buffer_height = capture_height - .max(monitor_height) - .min(MAX_GAME_CAPTURE_DIMENSION); - let frame_map_size = shared_frame_mapping_size(buffer_width, buffer_height)?; - if frame_map_size > MAX_GAME_CAPTURE_FRAME_BYTES { - return None; - } - Some((buffer_width, buffer_height)) -} - -const SHARED_TEXTURE_ADAPTER_LIMIT: u32 = 16; - -fn shared_texture_adapter_candidates() -> Vec> { - let mut candidates: Vec> = - Vec::with_capacity(1 + SHARED_TEXTURE_ADAPTER_LIMIT as usize); - candidates.push(None); - if let Ok(factory) = unsafe { CreateDXGIFactory1::() } { - let mut index = 0u32; - while index < SHARED_TEXTURE_ADAPTER_LIMIT { - match unsafe { factory.EnumAdapters(index) } { - Ok(adapter) => candidates.push(Some(adapter)), - Err(_) => break, - } - index += 1; - } - } - assert!(!candidates.is_empty(), "default device candidate present"); - assert!( - candidates.len() <= 1 + SHARED_TEXTURE_ADAPTER_LIMIT as usize, - "adapter candidates bounded" - ); - candidates -} - -impl SharedTextureReader { - fn new( - handle: u64, - width: u32, - height: u32, - format: u32, - capture_flags: u32, - output_width: u32, - output_height: u32, - ) -> Result { - assert!(handle != 0, "shared texture handle is non-zero"); - let source_format = crate::hdr::SourceFormat::classify(format, capture_flags) - .ok_or_else(|| format!("unsupported game shared texture DXGI format {format}"))?; - let mut last_error = None; - for adapter in shared_texture_adapter_candidates() { - let (device, context) = match create_shared_texture_device(adapter.as_ref()) { - Ok(created) => created, - Err(error) => { - last_error = Some(error); - continue; - } - }; - match Self::new_with_device( - device, - context, - handle, - width, - height, - format, - source_format, - output_width, - output_height, - ) { - Ok(reader) => return Ok(reader), - Err(error) => last_error = Some(error), - } - } - Err(last_error.unwrap_or_else(|| "no D3D11 devices available for shared texture".into())) - } - - #[allow(clippy::too_many_arguments)] - fn new_with_device( - device: ID3D11Device, - context: ID3D11DeviceContext, - handle: u64, - width: u32, - height: u32, - format: u32, - source_format: crate::hdr::SourceFormat, - output_width: u32, - output_height: u32, - ) -> Result { - assert!(handle != 0, "shared texture handle is non-zero"); - assert!(width > 0, "shared texture width is positive"); - assert!(height > 0, "shared texture height is positive"); - let mut texture = None; - unsafe { - device - .OpenSharedResource::( - WinHandle(handle as usize as *mut c_void), - &mut texture, - ) - .map_err(|e| format!("OpenSharedResource game texture: {e}"))?; - } - let texture = texture.ok_or("OpenSharedResource returned no game texture")?; - let mut desc = D3D11_TEXTURE2D_DESC::default(); - unsafe { - texture.GetDesc(&mut desc); - } - if desc.Width != width || desc.Height != height || desc.Format.0 as u32 != format { - return Err(format!( - "game shared texture metadata mismatch: info={width}x{height}/{format}, texture={}x{}/{}", - desc.Width, desc.Height, desc.Format.0 - )); - } - let nv12 = if env_flag_enabled(ENV_FORCE_CPU) { - None - } else { - Nv12GpuConverter::new( - &device, - &context, - &texture, - width, - height, - output_width, - output_height, - source_format, - ) - }; - Ok(Self { - device, - context, - handle, - width, - height, - format, - source_format, - nv12, - }) - } - - fn rebuild_on_existing_device( - existing: SharedTextureReader, - info: &GameCaptureSharedInfo, - output_width: u32, - output_height: u32, - ) -> Result { - assert!( - info.texture_handle != 0, - "shared texture handle is non-zero" - ); - assert!(output_width > 0, "output width is positive"); - let source_format = - crate::hdr::SourceFormat::classify(info.dxgi_format, info.capture_flags).ok_or_else( - || { - format!( - "unsupported game shared texture DXGI format {}", - info.dxgi_format - ) - }, - )?; - Self::new_with_device( - existing.device, - existing.context, - info.texture_handle, - info.width, - info.height, - info.dxgi_format, - source_format, - output_width, - output_height, - ) - } - - fn matches( - &self, - handle: u64, - width: u32, - height: u32, - format: u32, - capture_flags: u32, - ) -> bool { - self.handle == handle - && self.width == width - && self.height == height - && self.format == format - && Some(self.source_format) == crate::hdr::SourceFormat::classify(format, capture_flags) - } -} - -fn emit_shared_texture_to_bus( - reader: &mut Option, - session: &GameCaptureSession, - info: &GameCaptureSharedInfo, - inner: &CaptureInner, - frame_sink: &crate::FrameSinkRef, -) -> Result { - let reader = shared_texture_reader(reader, session, info)?; - let timestamp_us = shared_texture_timestamp(info); - - if let Some(nv12) = reader.nv12.as_mut() { - match nv12.convert_shared_texture() { - Ok(frame) => { - session.set_native_texture_info(Some(NativeTextureHandleInfo { - handle: frame.handle, - width: frame.width, - height: frame.height, - dxgi_format: frame.dxgi_format, - timestamp_us, - })); - return Ok(emit_shared_texture_frame( - inner, - frame_sink, - frame.handle, - frame.width, - frame.height, - frame.dxgi_format, - timestamp_us, - )); - } - Err(error) => { - verbose_log(&format!( - "disabling GPU NV12 conversion for this shared texture after failure: {error}" - )); - reader.nv12 = None; - session.set_native_texture_info(None); - } - } - } - - session.set_native_texture_info(Some(NativeTextureHandleInfo { - handle: reader.handle, - width: reader.width, - height: reader.height, - dxgi_format: reader.format, - timestamp_us, - })); - Ok(emit_shared_texture_frame( - inner, - frame_sink, - reader.handle, - reader.width, - reader.height, - reader.format, - timestamp_us, - )) -} - -fn shared_texture_reader<'a>( - reader: &'a mut Option, - session: &GameCaptureSession, - info: &GameCaptureSharedInfo, -) -> Result<&'a mut SharedTextureReader, String> { - let handle = info.texture_handle; - if handle == 0 { - return Err("game shared texture handle was empty".into()); - } - if reader.as_ref().map(|reader| { - reader.matches( - handle, - info.width, - info.height, - info.dxgi_format, - info.capture_flags, - ) - }) != Some(true) - { - let reused = reader.take().and_then(|existing| { - SharedTextureReader::rebuild_on_existing_device( - existing, - info, - session.output_width, - session.output_height, - ) - .ok() - }); - *reader = Some(match reused { - Some(rebuilt) => rebuilt, - None => SharedTextureReader::new( - handle, - info.width, - info.height, - info.dxgi_format, - info.capture_flags, - session.output_width, - session.output_height, - )?, - }); - } - let reader = reader - .as_mut() - .ok_or_else(|| "game shared texture reader was not initialized".to_string())?; - Ok(reader) -} - -fn shared_texture_timestamp(info: &GameCaptureSharedInfo) -> i64 { - if info.timestamp_us > 0 { - info.timestamp_us - } else { - wall_clock_us() - } -} - -struct StallTracker { - last_frame_counter: Option, - last_change_at: std::time::Instant, - stall_emitted: bool, -} - -impl StallTracker { - fn new() -> Self { - Self { - last_frame_counter: None, - last_change_at: std::time::Instant::now(), - stall_emitted: false, - } - } - - fn observe( - &mut self, - inner: &Arc, - session: &GameCaptureSession, - info: &GameCaptureSharedInfo, - ) { - if info.magic != crate::game_capture_abi::GAME_CAPTURE_MAGIC { - self.last_change_at = std::time::Instant::now(); - return; - } - - let counter = info.frame_counter; - let advanced = match self.last_frame_counter { - Some(previous) => counter != previous, - None => { - self.last_frame_counter = Some(counter); - self.last_change_at = std::time::Instant::now(); - return; - } - }; - - if advanced { - self.last_frame_counter = Some(counter); - self.last_change_at = std::time::Instant::now(); - if self.stall_emitted { - self.stall_emitted = false; - emit_lifecycle( - inner, - "diagnostic", - "game capture frames resumed after a stall", - ); - } - return; - } - - if self.stall_emitted { - return; - } - if self.last_change_at.elapsed() < STALL_THRESHOLD { - return; - } - if unsafe { IsWindow(session.target_hwnd) } == 0 { - return; - } - if unsafe { IsWindowVisible(session.target_hwnd) } == 0 { - return; - } - - let presenting_recently = presented_recently( - info.present_clock, - info.last_present_timestamp_us, - wall_clock_us(), - qpc_now_us(), - RECENT_PRESENT_WINDOW_US, - ); - let stalled_ms = self.last_change_at.elapsed().as_millis(); - let detail = if presenting_recently { - format!( - "game capture stalled: producer is still presenting (api={}, transport={}) but \ - has published no new frame for {stalled_ms}ms; the fast path appears to be \ - failing (fallback_reason={}, dropped={})", - info.api_type, info.transport, info.fallback_reason, info.dropped_frame_counter - ) - } else { - format!( - "game capture stalled: no presents observed for {stalled_ms}ms (api={}, \ - transport={}, last_present_us={}); the game may be paused, occluded, or no \ - longer rendering", - info.api_type, info.transport, info.last_present_timestamp_us - ) - }; - emit_lifecycle(inner, "stalled", &detail); - self.stall_emitted = true; - } -} - -pub fn capture_loop(inner: &Arc, _frame_interval: std::time::Duration) { - let session = { - let guard = inner.game_session.lock(); - match guard.as_ref() { - Some(session) => Arc::clone(session), - None => { - emit_lifecycle(inner, "closed-clean", "no game capture session"); - return; - } - } - }; - - { - let requested = session.requested_injection_method(); - let used = session.used_injection_method(); - let detail = if requested == used { - format!("game capture injected via {used}") - } else { - format!("game capture injected via {used} (requested {requested})") - }; - emit_lifecycle(inner, "diagnostic", &detail); - } - - let capture_id = inner.capture_id.lock().clone(); - let mut shared_texture_reader: Option = None; - let mut stall_tracker = StallTracker::new(); - while inner.running.load(Ordering::Acquire) { - if unsafe { IsWindow(session.target_hwnd) } == 0 { - emit_lifecycle(inner, "closed", "game window closed"); - break; - } - let wait = unsafe { WaitForSingleObject(session.ready_event.raw(), FRAME_EVENT_WAIT_MS) }; - if wait == WAIT_TIMEOUT { - let info = unsafe { std::ptr::read_volatile(session.info.ptr) }; - stall_tracker.observe(inner, &session, &info); - continue; - } - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - emit_lifecycle(inner, "error", "game capture frame event wait failed"); - break; - } - let info = unsafe { std::ptr::read_volatile(session.info.ptr) }; - stall_tracker.observe(inner, &session, &info); - if info.state == GAME_CAPTURE_STATE_ERROR { - emit_lifecycle( - inner, - "diagnostic", - &format!("game capture hook reported error {}", info.last_error), - ); - if !handle_fallback_signature(inner, fallback::FailureSignature::DeviceLost) { - break; - } - continue; - } - if info.state == GAME_CAPTURE_STATE_RESIZE_REQUIRED { - handle_fallback_signature(inner, fallback::FailureSignature::UnsupportedFormat); - break; - } - if info.state == GAME_CAPTURE_STATE_STOPPED { - emit_lifecycle(inner, "closed", "game capture hook stopped"); - break; - } - if info.width == 0 || info.height == 0 || info.pitch == 0 { - continue; - } - if info.width > session.capture_width || info.height > session.capture_height { - emit_lifecycle(inner, "error", "game capture frame exceeded shared buffer"); - handle_fallback_signature(inner, fallback::FailureSignature::UnsupportedFormat); - break; - } - let Some(frame_sink) = crate::resolve_frame_sink(inner, capture_id.as_deref()) else { - note_media_frame_without_sink( - inner, - "Windows game capture frame dropped because no native frame sink is registered", - ); - continue; - }; - if info.transport == GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE { - match emit_shared_texture_to_bus( - &mut shared_texture_reader, - &session, - &info, - inner, - &frame_sink, - ) { - Ok(_) => continue, - Err(error) => { - if info.api_type == GAME_CAPTURE_API_OPENGL { - emit_lifecycle( - inner, - "diagnostic", - &format!( - "OpenGL shared texture was rejected by the parent reader; \ - requesting hook CPU fallback: {error}" - ), - ); - session.request_hook_disable_shared_texture(); - shared_texture_reader = None; - continue; - } - emit_lifecycle(inner, "error", &error); - handle_fallback_signature( - inner, - fallback::FailureSignature::UnsupportedTransport, - ); - break; - } - } - } - if info.transport != GAME_CAPTURE_TRANSPORT_MEMORY { - emit_lifecycle( - inner, - "error", - "game capture hook used unknown frame transport", - ); - handle_fallback_signature(inner, fallback::FailureSignature::UnsupportedTransport); - break; - } - note_cpu_fallback_frame_dropped( - inner, - "Windows game capture CPU-memory transport dropped; native sender requires shared textures", - ); - } - - inner.running.store(false, Ordering::Release); - emit_lifecycle(inner, "closed-clean", "game capture stopped"); -} - -fn handle_fallback_signature( - inner: &Arc, - signature: fallback::FailureSignature, -) -> bool { - matches!( - crate::observe_fallback(inner, signature), - Some(fallback::FallbackDecision::Stay { - retry_in_place: true - }) - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn shared_buffer_grows_to_nearest_monitor_for_window_resize_recovery() { - assert_eq!( - choose_shared_buffer_dimensions(640, 360, 1920, 1080), - Some((1920, 1080)) - ); - } - - #[test] - fn shared_buffer_keeps_initial_size_when_window_is_larger_than_monitor() { - assert_eq!( - choose_shared_buffer_dimensions(2560, 1440, 1920, 1080), - Some((2560, 1440)) - ); - } - - #[test] - fn shared_buffer_rejects_unreasonably_large_monitor_mapping() { - assert_eq!( - choose_shared_buffer_dimensions( - 640, - 360, - MAX_GAME_CAPTURE_DIMENSION, - MAX_GAME_CAPTURE_DIMENSION - ), - None - ); - } - - #[test] - fn emulated_x64_target_on_arm64_host_is_not_classified_as_32_bit() { - assert_eq!(machine_is_32_bit(IMAGE_FILE_MACHINE_AMD64), Ok(false)); - assert_eq!(machine_is_32_bit(IMAGE_FILE_MACHINE_ARM64), Ok(false)); - } - - #[test] - fn wow64_guest_machines_are_classified_as_32_bit() { - assert_eq!(machine_is_32_bit(IMAGE_FILE_MACHINE_I386), Ok(true)); - assert_eq!(machine_is_32_bit(IMAGE_FILE_MACHINE_ARMNT), Ok(true)); - assert_eq!(machine_is_32_bit(IMAGE_FILE_MACHINE_ARM), Ok(true)); - } - - #[test] - fn unknown_target_machine_fails_loudly_instead_of_guessing_bitness() { - assert!(machine_is_32_bit(IMAGE_FILE_MACHINE_UNKNOWN).is_err()); - } -} diff --git a/fluxer_desktop/native/win-game-capture/src/lib.rs b/fluxer_desktop/native/win-game-capture/src/lib.rs index 14ef095b1..d8de56837 100644 --- a/fluxer_desktop/native/win-game-capture/src/lib.rs +++ b/fluxer_desktop/native/win-game-capture/src/lib.rs @@ -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>, #[cfg(target_os = "windows")] pub session: Mutex>, #[cfg(target_os = "windows")] pub(crate) wgc_session: Mutex>, - #[cfg(all(target_os = "windows", feature = "game-capture-hook"))] - pub game_session: Mutex>>, pub running: std::sync::atomic::AtomicBool, pub fallback: Mutex>, pub capture_id: Mutex>, @@ -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, height: Option, frame_rate: Option, - hook_path: Option, - hook_path_x86: Option, - injection_method: Option, capture_id: Option, start_options: Option, ) -> Result { @@ -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 { - #[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, height: Option, frame_rate: Option, - hook_path: Option, - hook_path_x86: Option, - injection_method: Option, _start_options: CaptureStartOptionsDiagnostics, ) -> Result { 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, - height: Option, - frame_rate: Option, - hook_path: Option, - hook_path_x86: Option, - injection_method: Option, - _start_options: CaptureStartOptionsDiagnostics, - ) -> Result { - 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) -> 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, - } - } -} diff --git a/fluxer_desktop/native/win-game-capture/src/vulkan_layer_registry.rs b/fluxer_desktop/native/win-game-capture/src/vulkan_layer_registry.rs deleted file mode 100644 index 5a405756c..000000000 --- a/fluxer_desktop/native/win-game-capture/src/vulkan_layer_registry.rs +++ /dev/null @@ -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 { - value.encode_utf16().chain(std::iter::once(0)).collect() -} - -fn manifest_dll_path(manifest_path: &Path) -> Option { - 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 { - 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::() 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 -} diff --git a/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.lock b/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.lock deleted file mode 100644 index 3cee8be35..000000000 --- a/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.toml b/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.toml deleted file mode 100644 index 1fda90b0c..000000000 --- a/fluxer_desktop/native/win-game-capture/vulkan-layer/Cargo.toml +++ /dev/null @@ -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", -]} diff --git a/fluxer_desktop/native/win-game-capture/vulkan-layer/src/d3d11_interop.rs b/fluxer_desktop/native/win-game-capture/vulkan-layer/src/d3d11_interop.rs deleted file mode 100644 index d187361af..000000000 --- a/fluxer_desktop/native/win-game-capture/vulkan-layer/src/d3d11_interop.rs +++ /dev/null @@ -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 { - 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 { - for driver in [D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP] { - let mut device: Option = None; - let mut context: Option = 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 { - 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 = 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, - }) - } -} diff --git a/fluxer_desktop/native/win-game-capture/vulkan-layer/src/lib.rs b/fluxer_desktop/native/win-game-capture/vulkan-layer/src/lib.rs deleted file mode 100644 index cbfcb00c2..000000000 --- a/fluxer_desktop/native/win-game-capture/vulkan-layer/src/lib.rs +++ /dev/null @@ -1,2631 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -#![cfg(target_os = "windows")] -#![allow(non_snake_case)] -#![allow(unsafe_op_in_unsafe_fn)] - -#[path = "../../src/game_capture_abi.rs"] -mod game_capture_abi; - -mod d3d11_interop; - -use ash::{khr, vk}; -use ash_layer::{ - LayerFunction, NegotiateLayerInterface, PFN_vk_layerGetPhysicalDeviceProcAddr, - PFN_vkNegotiateLoaderLayerInterfaceVersion, get_device_chain_info, get_instance_chain_info, -}; -use d3d11_interop::{D3d11Device, InteropFormat, SharedTexture, interop_format}; -use dashmap::DashMap; -use game_capture_abi::{ - ENV_FORCE_CPU, ENV_FORCE_SHARED_TEXTURE, GAME_CAPTURE_API_VULKAN, GAME_CAPTURE_BUFFER_COUNT, - GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED, GAME_CAPTURE_FALLBACK_FORCED_CPU, - GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED, GAME_CAPTURE_FALLBACK_NONE, GAME_CAPTURE_FLAG_HDR, - GAME_CAPTURE_FLAG_TEN_BIT, GAME_CAPTURE_FRAME_PREFIX, GAME_CAPTURE_INFO_PREFIX, - GAME_CAPTURE_KEEPALIVE_PREFIX, GAME_CAPTURE_MAGIC, GAME_CAPTURE_PRESENT_CLOCK_QPC, - GAME_CAPTURE_READY_PREFIX, GAME_CAPTURE_STATE_ACTIVE, GAME_CAPTURE_STATE_ERROR, - GAME_CAPTURE_STATE_RESIZE_REQUIRED, GAME_CAPTURE_STATE_STOPPED, GAME_CAPTURE_STOP_PREFIX, - GAME_CAPTURE_TRANSPORT_MEMORY, GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE, GameCaptureSharedInfo, - env_flag_enabled, frame_buffer_size, host_supports_present_clock, mutex_name, object_name, - qpc_now_us, -}; -use once_cell::sync::{Lazy, OnceCell}; -use std::{ - ffi::{CStr, c_char}, - mem, - ptr::{null, null_mut}, - sync::Mutex, -}; -use windows_sys::Win32::{ - Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_ABANDONED, WAIT_OBJECT_0}, - System::{ - Memory::{ - FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW, - UnmapViewOfFile, - }, - Threading::{ - EVENT_ALL_ACCESS, MUTEX_ALL_ACCESS, OpenEventW, OpenMutexW, ReleaseMutex, - SYNCHRONIZATION_SYNCHRONIZE, SetEvent, WaitForSingleObject, - }, - }, -}; - -const LAYER_NAME: &[u8] = b"VK_LAYER_FLUXER_game_capture\0"; -const FRAME_MUTEX_WAIT_MS: u32 = 0; -const CAPTURE_FENCE_TIMEOUT_NS: u64 = 250_000_000; - -const DEVICE_EXT_EXTERNAL_MEMORY: &[u8] = b"VK_KHR_external_memory\0"; -const DEVICE_EXT_EXTERNAL_MEMORY_WIN32: &[u8] = b"VK_KHR_external_memory_win32\0"; -const DEVICE_EXT_DEDICATED_ALLOCATION: &[u8] = b"VK_KHR_dedicated_allocation\0"; -const DEVICE_EXT_GET_MEMORY_REQUIREMENTS2: &[u8] = b"VK_KHR_get_memory_requirements2\0"; - -const INSTANCE_EXT_PHYSICAL_DEVICE_PROPERTIES2: &[u8] = b"VK_KHR_get_physical_device_properties2\0"; -const INSTANCE_EXT_EXTERNAL_MEMORY_CAPABILITIES: &[u8] = b"VK_KHR_external_memory_capabilities\0"; - -const SHARED_HANDLE_TYPE: vk::ExternalMemoryHandleTypeFlags = - vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE_KMT; - -static VERBOSE: Lazy = - Lazy::new(|| game_capture_abi::env_flag_enabled(game_capture_abi::ENV_VERBOSE)); - -macro_rules! vlog { - ($($arg:tt)*) => { - if *VERBOSE { - eprintln!("[fluxer-vk-layer] {}", format!($($arg)*)); - } - }; -} - -static GIPA: OnceCell = OnceCell::new(); -static GPHYPA: OnceCell = OnceCell::new(); -static ENTRY: OnceCell = OnceCell::new(); - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum CaptureMode { - Auto, - ForceCpu, - ForceSharedTexture, -} - -fn capture_mode() -> CaptureMode { - static MODE: OnceCell = OnceCell::new(); - let value = *MODE.get_or_init(|| { - if env_flag_enabled(ENV_FORCE_CPU) { - 0 - } else if env_flag_enabled(ENV_FORCE_SHARED_TEXTURE) { - 2 - } else { - 1 - } - }); - match value { - 0 => CaptureMode::ForceCpu, - 2 => CaptureMode::ForceSharedTexture, - _ => CaptureMode::Auto, - } -} - -static INSTANCE_MAP: Lazy> = Lazy::new(DashMap::new); -static PHY_TO_INSTANCE_MAP: Lazy> = - Lazy::new(DashMap::new); -static GDPA_MAP: Lazy> = Lazy::new(DashMap::new); -static DEVICE_MAP: Lazy> = Lazy::new(DashMap::new); -static SURFACE_MAP: Lazy> = Lazy::new(DashMap::new); -static SWAPCHAIN_MAP: Lazy> = Lazy::new(DashMap::new); -static QUEUE_MAP: Lazy> = Lazy::new(DashMap::new); -static IPC_STATE: Lazy>> = Lazy::new(|| Mutex::new(None)); -static D3D11_DEVICE: Lazy>> = Lazy::new(|| Mutex::new(None)); -static FAST_PATH_DISABLED: Lazy> = Lazy::new(|| Mutex::new(false)); - -#[derive(Clone)] -struct LayerInstance { - ash_instance: ash::Instance, - khr_surface: khr::surface::Instance, - khr_win32_surface: khr::win32_surface::Instance, -} - -#[derive(Clone)] -struct LayerDevice { - instance: vk::Instance, - physical_device: vk::PhysicalDevice, - ash_device: ash::Device, - khr_swapchain: khr::swapchain::Device, - queue_families: Vec, - khr_external_memory_win32: Option, - external_memory_enabled: bool, - dedicated_allocation_enabled: bool, -} - -#[derive(Clone, Copy)] -struct SurfaceState { - instance: vk::Instance, - hwnd: vk::HWND, -} - -#[derive(Clone)] -struct SwapchainState { - device: vk::Device, - hwnd: vk::HWND, - extent: vk::Extent2D, - format: vk::Format, - images: Vec, - can_capture: bool, - present_layout: vk::ImageLayout, - shared_texture_allowed: bool, -} - -struct QueueState { - device: vk::Device, - family_index: u32, - supports_transfer: bool, - resources: Mutex>, - shared: Mutex>, -} - -struct ReadbackResources { - width: u32, - height: u32, - format_mode: FormatMode, - buffer: vk::Buffer, - memory: vk::DeviceMemory, - command_pool: vk::CommandPool, - command_buffer: vk::CommandBuffer, - fence: vk::Fence, - present_wait_semaphore: vk::Semaphore, - size: vk::DeviceSize, -} - -struct SharedTextureResources { - width: u32, - height: u32, - format: InteropFormat, - shared_texture: SharedTexture, - image: vk::Image, - memory: vk::DeviceMemory, - command_pool: vk::CommandPool, - command_buffer: vk::CommandBuffer, - fence: vk::Fence, - present_wait_semaphore: vk::Semaphore, - initialised: bool, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum FormatMode { - Bgra, - Rgba, -} - -struct CaptureSubmit { - chained_wait: Option, -} - -impl CaptureSubmit { - fn none() -> Self { - Self { chained_wait: None } - } -} - -struct IpcState { - info_map: HANDLE, - frame_map: HANDLE, - ready_event: HANDLE, - stop_event: HANDLE, - mutexes: [HANDLE; GAME_CAPTURE_BUFFER_COUNT], - info: *mut GameCaptureSharedInfo, - frame_base: *mut u8, - frame_buffer_capacity: usize, - next_frame_index: usize, - last_frame_ns: u64, - keepalive_name: Vec, -} - -unsafe impl Send for IpcState {} - -fn wide(value: &str) -> Vec { - value.encode_utf16().chain(std::iter::once(0)).collect() -} - -struct AugmentedExtensions { - pointers: Vec<*const c_char>, -} - -impl AugmentedExtensions { - unsafe fn build( - existing: *const *const c_char, - existing_count: u32, - extra: &[&'static [u8]], - ) -> Self { - let mut pointers: Vec<*const c_char> = Vec::new(); - let mut present: Vec<&CStr> = Vec::new(); - if !existing.is_null() { - let slice = std::slice::from_raw_parts(existing, existing_count as usize); - for &ptr in slice { - pointers.push(ptr); - if !ptr.is_null() { - present.push(CStr::from_ptr(ptr)); - } - } - } - for &name in extra { - let cstr = CStr::from_bytes_with_nul_unchecked(name); - if !present.contains(&cstr) { - pointers.push(cstr.as_ptr()); - } - } - Self { pointers } - } - - fn count(&self) -> u32 { - self.pointers.len() as u32 - } - - fn as_ptr(&self) -> *const *const c_char { - self.pointers.as_ptr() - } -} - -unsafe fn extension_in_list(list: *const *const c_char, count: u32, name: &'static [u8]) -> bool { - if list.is_null() { - return false; - } - let target = CStr::from_bytes_with_nul_unchecked(name); - std::slice::from_raw_parts(list, count as usize) - .iter() - .any(|&ptr| !ptr.is_null() && CStr::from_ptr(ptr) == target) -} - -fn current_pid() -> u32 { - std::process::id() -} - -fn now_ns() -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos() as u64) - .unwrap_or(0) -} - -fn now_us() -> i64 { - (now_ns() / 1_000) as i64 -} - -unsafe fn close_handle(handle: &mut HANDLE) { - if !handle.is_null() && *handle != INVALID_HANDLE_VALUE { - CloseHandle(*handle); - *handle = null_mut(); - } -} - -unsafe fn open_event(prefix: &str, pid: u32) -> HANDLE { - let name = wide(&object_name(prefix, pid)); - OpenEventW(EVENT_ALL_ACCESS, 0, name.as_ptr()) -} - -unsafe fn open_mutex(name: &str) -> HANDLE { - let name = wide(name); - OpenMutexW(MUTEX_ALL_ACCESS, 0, name.as_ptr()) -} - -unsafe fn parent_alive(keepalive_name: &[u16]) -> bool { - let handle = OpenMutexW(SYNCHRONIZATION_SYNCHRONIZE, 0, keepalive_name.as_ptr()); - if handle.is_null() { - return false; - } - CloseHandle(handle); - true -} - -unsafe fn map_view(handle: HANDLE, size: usize) -> *mut T { - let view = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, size); - view.Value.cast() -} - -unsafe fn free_ipc_state(state: &mut IpcState) { - if !state.info.is_null() { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: state.info.cast(), - }); - state.info = null_mut(); - } - if !state.frame_base.is_null() { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: state.frame_base.cast(), - }); - state.frame_base = null_mut(); - } - close_handle(&mut state.mutexes[0]); - close_handle(&mut state.mutexes[1]); - close_handle(&mut state.stop_event); - close_handle(&mut state.ready_event); - close_handle(&mut state.frame_map); - close_handle(&mut state.info_map); -} - -unsafe fn open_ipc() -> Option { - let pid = current_pid(); - let info_name = wide(&object_name(GAME_CAPTURE_INFO_PREFIX, pid)); - let frame_name = wide(&object_name(GAME_CAPTURE_FRAME_PREFIX, pid)); - let info_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, info_name.as_ptr()); - if info_map.is_null() { - return None; - } - let frame_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, frame_name.as_ptr()); - if frame_map.is_null() { - let mut handle = info_map; - close_handle(&mut handle); - return None; - } - let info = map_view::(info_map, mem::size_of::()); - if info.is_null() || (*info).magic != GAME_CAPTURE_MAGIC { - let mut info_map = info_map; - let mut frame_map = frame_map; - close_handle(&mut frame_map); - close_handle(&mut info_map); - return None; - } - let frame_buffer_capacity = frame_buffer_size((*info).max_width, (*info).max_height)?; - let frame_map_size = frame_buffer_capacity.checked_mul(GAME_CAPTURE_BUFFER_COUNT)?; - let frame_base = map_view::(frame_map, frame_map_size); - if frame_base.is_null() { - let mut info_map = info_map; - let mut frame_map = frame_map; - close_handle(&mut frame_map); - close_handle(&mut info_map); - return None; - } - - let ready_event = open_event(GAME_CAPTURE_READY_PREFIX, pid); - let stop_event = open_event(GAME_CAPTURE_STOP_PREFIX, pid); - let mutex_names = [mutex_name(pid, 0), mutex_name(pid, 1)]; - let mutexes = [open_mutex(&mutex_names[0]), open_mutex(&mutex_names[1])]; - if ready_event.is_null() - || stop_event.is_null() - || mutexes.iter().any(|handle| handle.is_null()) - { - let mut state = IpcState { - info_map, - frame_map, - ready_event, - stop_event, - mutexes, - info, - frame_base, - frame_buffer_capacity, - next_frame_index: 0, - last_frame_ns: 0, - keepalive_name: wide(&object_name(GAME_CAPTURE_KEEPALIVE_PREFIX, pid)), - }; - free_ipc_state(&mut state); - return None; - } - - Some(IpcState { - info_map, - frame_map, - ready_event, - stop_event, - mutexes, - info, - frame_base, - frame_buffer_capacity, - next_frame_index: 0, - last_frame_ns: 0, - keepalive_name: wide(&object_name(GAME_CAPTURE_KEEPALIVE_PREFIX, pid)), - }) -} - -unsafe fn capture_should_run(state: &mut IpcState) -> bool { - if WaitForSingleObject(state.stop_event, 0) == WAIT_OBJECT_0 { - (*state.info).state = GAME_CAPTURE_STATE_STOPPED; - return false; - } - if !parent_alive(&state.keepalive_name) { - (*state.info).state = GAME_CAPTURE_STATE_STOPPED; - return false; - } - true -} - -unsafe fn frame_due(state: &mut IpcState) -> bool { - let interval = (*state.info).target_frame_interval_ns; - if interval == 0 { - return true; - } - let now = now_ns(); - if now.saturating_sub(state.last_frame_ns) < interval { - return false; - } - state.last_frame_ns = now; - true -} - -unsafe fn target_matches(state: &IpcState, hwnd: vk::HWND) -> bool { - let target = (*state.info).hwnd; - target == 0 || hwnd == 0 || target == hwnd as u64 -} - -unsafe fn write_rows_to_shared_memory( - state: &mut IpcState, - hwnd: vk::HWND, - width: u32, - height: u32, - src_base: *const u8, - src_row_pitch: usize, - format_mode: FormatMode, - fallback_reason: u32, -) -> bool { - if src_base.is_null() { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 60; - return false; - } - let max_width = (*state.info).max_width; - let max_height = (*state.info).max_height; - if width == 0 || height == 0 || width > max_width || height > max_height { - (*state.info).state = GAME_CAPTURE_STATE_RESIZE_REQUIRED; - (*state.info).width = width; - (*state.info).height = height; - return false; - } - let Some(bytes) = frame_buffer_size(width, height) else { - (*state.info).state = GAME_CAPTURE_STATE_ERROR; - (*state.info).last_error = 61; - return false; - }; - if bytes > state.frame_buffer_capacity { - (*state.info).state = GAME_CAPTURE_STATE_RESIZE_REQUIRED; - (*state.info).width = width; - (*state.info).height = height; - return false; - } - - let mut index = state.next_frame_index; - let mut mutex = state.mutexes[index]; - let mut wait = WaitForSingleObject(mutex, FRAME_MUTEX_WAIT_MS); - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - index = (index + 1) % GAME_CAPTURE_BUFFER_COUNT; - mutex = state.mutexes[index]; - wait = WaitForSingleObject(mutex, FRAME_MUTEX_WAIT_MS); - } - if wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED { - return false; - } - - let row_bytes = (width * 4) as usize; - let dst_base = state.frame_base.add(state.frame_buffer_capacity * index); - for y in 0..height as usize { - let src = src_base.add(y * src_row_pitch); - let dst = dst_base.add(y * row_bytes); - match format_mode { - FormatMode::Bgra => std::ptr::copy_nonoverlapping(src, dst, row_bytes), - FormatMode::Rgba => { - for x in 0..width as usize { - let src_px = src.add(x * 4); - let dst_px = dst.add(x * 4); - *dst_px.add(0) = *src_px.add(2); - *dst_px.add(1) = *src_px.add(1); - *dst_px.add(2) = *src_px.add(0); - *dst_px.add(3) = *src_px.add(3); - } - } - } - } - - let info = &mut *state.info; - info.hwnd = hwnd as u64; - info.width = width; - info.height = height; - info.pitch = width * 4; - info.frame_index = index as u32; - info.frame_counter = info.frame_counter.wrapping_add(1); - info.timestamp_us = now_us(); - info.transport = GAME_CAPTURE_TRANSPORT_MEMORY; - info.dxgi_format = 0; - info.texture_handle = 0; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.fallback_reason = fallback_reason; - info.capture_flags = 0; - info.state = GAME_CAPTURE_STATE_ACTIVE; - state.next_frame_index = (index + 1) % GAME_CAPTURE_BUFFER_COUNT; - - ReleaseMutex(mutex); - SetEvent(state.ready_event); - true -} - -fn format_mode(format: vk::Format) -> Option { - match format { - vk::Format::B8G8R8A8_UNORM | vk::Format::B8G8R8A8_SRGB => Some(FormatMode::Bgra), - vk::Format::R8G8B8A8_UNORM | vk::Format::R8G8B8A8_SRGB => Some(FormatMode::Rgba), - _ => None, - } -} - -fn hdr_format_flags(format: vk::Format) -> u32 { - match format { - vk::Format::A2B10G10R10_UNORM_PACK32 - | vk::Format::A2R10G10B10_UNORM_PACK32 - | vk::Format::A2B10G10R10_SNORM_PACK32 - | vk::Format::A2R10G10B10_SNORM_PACK32 => GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR, - vk::Format::R16G16B16A16_SFLOAT | vk::Format::R16G16B16A16_UNORM => GAME_CAPTURE_FLAG_HDR, - _ => 0, - } -} - -unsafe fn destroy_shared_texture_resources( - device: &ash::Device, - resources: &mut SharedTextureResources, -) { - let _ = device.wait_for_fences(&[resources.fence], true, CAPTURE_FENCE_TIMEOUT_NS); - device.destroy_fence(resources.fence, None); - if resources.present_wait_semaphore != vk::Semaphore::null() { - device.destroy_semaphore(resources.present_wait_semaphore, None); - resources.present_wait_semaphore = vk::Semaphore::null(); - } - device.destroy_command_pool(resources.command_pool, None); - device.destroy_image(resources.image, None); - device.free_memory(resources.memory, None); - resources.fence = vk::Fence::null(); - resources.command_pool = vk::CommandPool::null(); - resources.image = vk::Image::null(); - resources.memory = vk::DeviceMemory::null(); -} - -unsafe fn query_external_image_support( - instance: &ash::Instance, - physical_device: vk::PhysicalDevice, - format: vk::Format, -) -> Result { - let mut external_info = - vk::PhysicalDeviceExternalImageFormatInfo::default().handle_type(SHARED_HANDLE_TYPE); - let format_info = vk::PhysicalDeviceImageFormatInfo2::default() - .format(format) - .ty(vk::ImageType::TYPE_2D) - .tiling(vk::ImageTiling::OPTIMAL) - .usage(vk::ImageUsageFlags::TRANSFER_DST) - .flags(vk::ImageCreateFlags::empty()) - .push_next(&mut external_info); - let mut external_props = vk::ExternalImageFormatProperties::default(); - let mut props = vk::ImageFormatProperties2::default().push_next(&mut external_props); - instance.get_physical_device_image_format_properties2( - physical_device, - &format_info, - &mut props, - )?; - - let features = external_props - .external_memory_properties - .external_memory_features; - if !features.contains(vk::ExternalMemoryFeatureFlags::IMPORTABLE) { - return Err(vk::Result::ERROR_FORMAT_NOT_SUPPORTED); - } - Ok(features.contains(vk::ExternalMemoryFeatureFlags::DEDICATED_ONLY)) -} - -unsafe fn create_shared_texture_resources( - device_state: &LayerDevice, - queue_family_index: u32, - width: u32, - height: u32, - format: InteropFormat, -) -> Result { - if !device_state.external_memory_enabled || device_state.khr_external_memory_win32.is_none() { - return Err(vk::Result::ERROR_EXTENSION_NOT_PRESENT); - } - let device = &device_state.ash_device; - let Some(instance_state) = INSTANCE_MAP.get(&device_state.instance) else { - return Err(vk::Result::ERROR_INITIALIZATION_FAILED); - }; - let instance = instance_state.ash_instance.clone(); - drop(instance_state); - - let dedicated_required = match query_external_image_support( - &instance, - device_state.physical_device, - format.vk_format, - ) { - Ok(v) => { - vlog!("step query_external_image_support OK (dedicated_required={v})"); - v - } - Err(e) => { - vlog!( - "step query_external_image_support advisory-fail {e:?} (handle_type=D3D11_TEXTURE_KMT, format={:?}); attempting import anyway", - format.vk_format - ); - true - } - }; - - let shared_texture = { - let mut guard = D3D11_DEVICE - .lock() - .map_err(|_| vk::Result::ERROR_INITIALIZATION_FAILED)?; - if guard.is_none() { - *guard = D3d11Device::create(); - } - let d3d11 = guard - .as_ref() - .ok_or(vk::Result::ERROR_INITIALIZATION_FAILED)?; - match d3d11.create_shared_texture(width, height, format) { - Some(tex) => { - vlog!("step create_shared_texture OK (handle={:#x})", tex.handle); - tex - } - None => { - vlog!("step create_shared_texture FAILED (D3D11 CreateTexture2D/GetSharedHandle)"); - return Err(vk::Result::ERROR_OUT_OF_DEVICE_MEMORY); - } - } - }; - - let mut external_image_info = - vk::ExternalMemoryImageCreateInfo::default().handle_types(SHARED_HANDLE_TYPE); - let image_info = vk::ImageCreateInfo { - image_type: vk::ImageType::TYPE_2D, - format: format.vk_format, - extent: vk::Extent3D { - width, - height, - depth: 1, - }, - mip_levels: 1, - array_layers: 1, - samples: vk::SampleCountFlags::TYPE_1, - tiling: vk::ImageTiling::OPTIMAL, - usage: vk::ImageUsageFlags::TRANSFER_DST, - sharing_mode: vk::SharingMode::EXCLUSIVE, - initial_layout: vk::ImageLayout::UNDEFINED, - ..Default::default() - } - .push_next(&mut external_image_info); - let image = match device.create_image(&image_info, None) { - Ok(image) => image, - Err(e) => { - vlog!("step create_image (external) FAILED {e:?}"); - return Err(e); - } - }; - - let cleanup_image = |device: &ash::Device, image: vk::Image, shared: SharedTexture| { - device.destroy_image(image, None); - drop(shared); - }; - - let requirements = device.get_image_memory_requirements(image); - let Some(instance_state) = INSTANCE_MAP.get(&device_state.instance) else { - cleanup_image(device, image, shared_texture); - return Err(vk::Result::ERROR_INITIALIZATION_FAILED); - }; - let memory_properties = instance_state - .ash_instance - .get_physical_device_memory_properties(device_state.physical_device); - drop(instance_state); - let Some(memory_type_index) = find_memory_type( - &memory_properties, - requirements.memory_type_bits, - vk::MemoryPropertyFlags::DEVICE_LOCAL, - ) - .or_else(|| { - find_memory_type( - &memory_properties, - requirements.memory_type_bits, - vk::MemoryPropertyFlags::empty(), - ) - }) else { - cleanup_image(device, image, shared_texture); - return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT); - }; - - let want_dedicated = dedicated_required || device_state.dedicated_allocation_enabled; - let mut dedicated_info = vk::MemoryDedicatedAllocateInfo::default().image(image); - let mut import_info = vk::ImportMemoryWin32HandleInfoKHR { - handle_type: SHARED_HANDLE_TYPE, - handle: shared_texture.handle as usize as vk::HANDLE, - ..Default::default() - }; - let mut allocate_info = vk::MemoryAllocateInfo { - allocation_size: requirements.size, - memory_type_index, - ..Default::default() - } - .push_next(&mut import_info); - if want_dedicated { - allocate_info = allocate_info.push_next(&mut dedicated_info); - } - let memory = match device.allocate_memory(&allocate_info, None) { - Ok(memory) => memory, - Err(error) => { - vlog!( - "step allocate_memory (import KMT handle) FAILED {error:?} (want_dedicated={want_dedicated}, mem_type={memory_type_index})" - ); - cleanup_image(device, image, shared_texture); - return Err(error); - } - }; - if let Err(error) = device.bind_image_memory(image, memory, 0) { - vlog!("step bind_image_memory FAILED {error:?}"); - device.free_memory(memory, None); - cleanup_image(device, image, shared_texture); - return Err(error); - } - vlog!("shared-texture resources created OK (fast path live)"); - - let pool_info = vk::CommandPoolCreateInfo { - flags: vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, - queue_family_index, - ..Default::default() - }; - let command_pool = match device.create_command_pool(&pool_info, None) { - Ok(pool) => pool, - Err(error) => { - device.free_memory(memory, None); - cleanup_image(device, image, shared_texture); - return Err(error); - } - }; - let alloc_info = vk::CommandBufferAllocateInfo { - command_pool, - level: vk::CommandBufferLevel::PRIMARY, - command_buffer_count: 1, - ..Default::default() - }; - let command_buffer = match device.allocate_command_buffers(&alloc_info) { - Ok(mut buffers) => buffers.remove(0), - Err(error) => { - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - cleanup_image(device, image, shared_texture); - return Err(error); - } - }; - let fence = match device.create_fence(&vk::FenceCreateInfo::default(), None) { - Ok(fence) => fence, - Err(error) => { - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - cleanup_image(device, image, shared_texture); - return Err(error); - } - }; - let present_wait_semaphore = - match device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) { - Ok(semaphore) => semaphore, - Err(error) => { - device.destroy_fence(fence, None); - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - cleanup_image(device, image, shared_texture); - return Err(error); - } - }; - - Ok(SharedTextureResources { - width, - height, - format, - shared_texture, - image, - memory, - command_pool, - command_buffer, - fence, - present_wait_semaphore, - initialised: false, - }) -} - -unsafe fn ensure_shared_texture_resources<'a>( - device_state: &LayerDevice, - queue_state: &'a QueueState, - width: u32, - height: u32, - format: InteropFormat, -) -> Result>, vk::Result> { - let mut guard = queue_state - .shared - .lock() - .map_err(|_| vk::Result::ERROR_INITIALIZATION_FAILED)?; - let recreate = guard - .as_ref() - .map(|resources| { - resources.width != width - || resources.height != height - || resources.format.vk_format != format.vk_format - }) - .unwrap_or(true); - if recreate { - if let Some(mut old) = guard.take() { - destroy_shared_texture_resources(&device_state.ash_device, &mut old); - } - *guard = Some(create_shared_texture_resources( - device_state, - queue_state.family_index, - width, - height, - format, - )?); - } - Ok(guard) -} - -unsafe fn capture_swapchain_image_shared( - device_state: &LayerDevice, - queue_state: &QueueState, - queue: vk::Queue, - swapchain: &SwapchainState, - image_index: u32, - present_info: &vk::PresentInfoKHR, - ipc: &mut IpcState, -) -> Result { - if !swapchain.can_capture || !queue_state.supports_transfer { - return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT); - } - if !device_state.external_memory_enabled { - return Err(vk::Result::ERROR_EXTENSION_NOT_PRESENT); - } - let Some(format) = interop_format(swapchain.format) else { - return Err(vk::Result::ERROR_FORMAT_NOT_SUPPORTED); - }; - let Some(&image) = swapchain.images.get(image_index as usize) else { - return Err(vk::Result::ERROR_OUT_OF_DATE_KHR); - }; - let width = swapchain.extent.width; - let height = swapchain.extent.height; - if width == 0 || height == 0 { - return Err(vk::Result::ERROR_OUT_OF_DATE_KHR); - } - - let mut resources_guard = - ensure_shared_texture_resources(device_state, queue_state, width, height, format)?; - let resources = resources_guard - .as_mut() - .ok_or(vk::Result::ERROR_INITIALIZATION_FAILED)?; - let device = &device_state.ash_device; - let dst_image = resources.image; - let shared_handle = resources.shared_texture.handle; - - device.reset_fences(&[resources.fence])?; - device.reset_command_pool(resources.command_pool, vk::CommandPoolResetFlags::empty())?; - let begin_info = vk::CommandBufferBeginInfo { - flags: vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT, - ..Default::default() - }; - device.begin_command_buffer(resources.command_buffer, &begin_info)?; - - let range = vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }; - - let present_layout = swapchain.present_layout; - let shared_present = present_layout == vk::ImageLayout::SHARED_PRESENT_KHR; - let src_copy_layout = if shared_present { - vk::ImageLayout::SHARED_PRESENT_KHR - } else { - vk::ImageLayout::TRANSFER_SRC_OPTIMAL - }; - let mut pre_barriers: Vec = Vec::with_capacity(2); - if !shared_present { - pre_barriers.push(vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::MEMORY_READ, - dst_access_mask: vk::AccessFlags::TRANSFER_READ, - old_layout: present_layout, - new_layout: vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - image, - subresource_range: range, - ..Default::default() - }); - } - let dst_old_layout = if resources.initialised { - vk::ImageLayout::GENERAL - } else { - vk::ImageLayout::UNDEFINED - }; - let (dst_pre_src_queue_family, dst_pre_dst_queue_family) = - shared_texture_dst_pre_queue_families(resources.initialised, queue_state.family_index); - pre_barriers.push(vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::empty(), - dst_access_mask: vk::AccessFlags::TRANSFER_WRITE, - old_layout: dst_old_layout, - new_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, - src_queue_family_index: dst_pre_src_queue_family, - dst_queue_family_index: dst_pre_dst_queue_family, - image: dst_image, - subresource_range: range, - ..Default::default() - }); - device.cmd_pipeline_barrier( - resources.command_buffer, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::PipelineStageFlags::TRANSFER, - vk::DependencyFlags::empty(), - &[], - &[], - &pre_barriers, - ); - - let copy = vk::ImageCopy { - src_subresource: vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - src_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - dst_subresource: vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - extent: vk::Extent3D { - width, - height, - depth: 1, - }, - }; - device.cmd_copy_image( - resources.command_buffer, - image, - src_copy_layout, - dst_image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &[copy], - ); - - let mut post_barriers: Vec = Vec::with_capacity(2); - if !shared_present { - post_barriers.push(vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::TRANSFER_READ, - dst_access_mask: vk::AccessFlags::MEMORY_READ, - old_layout: vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - new_layout: present_layout, - src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - image, - subresource_range: range, - ..Default::default() - }); - } - post_barriers.push(vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::TRANSFER_WRITE, - dst_access_mask: vk::AccessFlags::empty(), - old_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL, - new_layout: vk::ImageLayout::GENERAL, - src_queue_family_index: queue_state.family_index, - dst_queue_family_index: vk::QUEUE_FAMILY_EXTERNAL, - image: dst_image, - subresource_range: range, - ..Default::default() - }); - device.cmd_pipeline_barrier( - resources.command_buffer, - vk::PipelineStageFlags::TRANSFER, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::DependencyFlags::empty(), - &[], - &[], - &post_barriers, - ); - device.end_command_buffer(resources.command_buffer)?; - - let chained_wait = submit_capture( - device, - queue, - resources.command_buffer, - resources.fence, - resources.present_wait_semaphore, - present_info, - )?; - resources.initialised = true; - - if device - .wait_for_fences(&[resources.fence], true, CAPTURE_FENCE_TIMEOUT_NS) - .is_err() - { - (*ipc.info).dropped_frame_counter = (*ipc.info).dropped_frame_counter.wrapping_add(1); - return Ok(CaptureSubmit { chained_wait }); - } - - publish_shared_texture(ipc, swapchain.hwnd, width, height, format, shared_handle); - Ok(CaptureSubmit { chained_wait }) -} - -fn shared_texture_dst_pre_queue_families(initialised: bool, queue_family_index: u32) -> (u32, u32) { - if initialised { - (vk::QUEUE_FAMILY_EXTERNAL, queue_family_index) - } else { - (vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED) - } -} - -unsafe fn submit_capture( - device: &ash::Device, - queue: vk::Queue, - command_buffer: vk::CommandBuffer, - fence: vk::Fence, - signal_semaphore: vk::Semaphore, - present_info: &vk::PresentInfoKHR, -) -> Result, vk::Result> { - let wait_semaphores = present_wait_semaphores(present_info); - let wait_stages = vec![vk::PipelineStageFlags::TRANSFER; wait_semaphores.len()]; - let chain = !wait_semaphores.is_empty(); - let signal = if chain { - [signal_semaphore] - } else { - [vk::Semaphore::null(); 1] - }; - let submit = vk::SubmitInfo { - wait_semaphore_count: wait_semaphores.len() as u32, - p_wait_semaphores: wait_semaphores.as_ptr(), - p_wait_dst_stage_mask: wait_stages.as_ptr(), - command_buffer_count: 1, - p_command_buffers: &command_buffer, - signal_semaphore_count: if chain { 1 } else { 0 }, - p_signal_semaphores: if chain { signal.as_ptr() } else { null() }, - ..Default::default() - }; - device.queue_submit(queue, &[submit], fence)?; - Ok(chain.then_some(signal_semaphore)) -} - -unsafe fn present_wait_semaphores<'a>( - present_info: &vk::PresentInfoKHR<'a>, -) -> &'a [vk::Semaphore] { - if present_info.wait_semaphore_count > 0 && !present_info.p_wait_semaphores.is_null() { - std::slice::from_raw_parts( - present_info.p_wait_semaphores, - present_info.wait_semaphore_count as usize, - ) - } else { - &[] - } -} - -unsafe fn publish_shared_texture( - state: &mut IpcState, - hwnd: vk::HWND, - width: u32, - height: u32, - format: InteropFormat, - shared_handle: u64, -) { - let info = &mut *state.info; - info.hwnd = hwnd as u64; - info.width = width; - info.height = height; - info.pitch = width * 4; - info.frame_index = 0; - info.frame_counter = info.frame_counter.wrapping_add(1); - info.timestamp_us = now_us(); - info.transport = GAME_CAPTURE_TRANSPORT_SHARED_TEXTURE; - info.dxgi_format = format.dxgi_format.0 as u32; - info.texture_handle = shared_handle; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.fallback_reason = GAME_CAPTURE_FALLBACK_NONE; - info.capture_flags = format.capture_flags; - info.state = GAME_CAPTURE_STATE_ACTIVE; - SetEvent(state.ready_event); -} - -unsafe fn destroy_readback_resources(device: &ash::Device, resources: &mut ReadbackResources) { - let _ = device.wait_for_fences(&[resources.fence], true, CAPTURE_FENCE_TIMEOUT_NS); - device.destroy_fence(resources.fence, None); - if resources.present_wait_semaphore != vk::Semaphore::null() { - device.destroy_semaphore(resources.present_wait_semaphore, None); - resources.present_wait_semaphore = vk::Semaphore::null(); - } - device.destroy_command_pool(resources.command_pool, None); - device.destroy_buffer(resources.buffer, None); - device.free_memory(resources.memory, None); - resources.fence = vk::Fence::null(); - resources.command_pool = vk::CommandPool::null(); - resources.buffer = vk::Buffer::null(); - resources.memory = vk::DeviceMemory::null(); -} - -fn find_memory_type( - properties: &vk::PhysicalDeviceMemoryProperties, - type_bits: u32, - required: vk::MemoryPropertyFlags, -) -> Option { - for index in 0..properties.memory_type_count { - let supported = (type_bits & (1 << index)) != 0; - let flags = properties.memory_types[index as usize].property_flags; - if supported && flags.contains(required) { - return Some(index); - } - } - None -} - -unsafe fn create_readback_resources( - device_state: &LayerDevice, - queue_family_index: u32, - width: u32, - height: u32, - format_mode: FormatMode, -) -> Result { - let device = &device_state.ash_device; - let size = (width as vk::DeviceSize) - .checked_mul(height as vk::DeviceSize) - .and_then(|value| value.checked_mul(4)) - .ok_or(vk::Result::ERROR_OUT_OF_HOST_MEMORY)?; - let buffer_info = vk::BufferCreateInfo { - size, - usage: vk::BufferUsageFlags::TRANSFER_DST, - sharing_mode: vk::SharingMode::EXCLUSIVE, - ..Default::default() - }; - let buffer = device.create_buffer(&buffer_info, None)?; - let requirements = device.get_buffer_memory_requirements(buffer); - let Some(instance_state) = INSTANCE_MAP.get(&device_state.instance) else { - device.destroy_buffer(buffer, None); - return Err(vk::Result::ERROR_INITIALIZATION_FAILED); - }; - let memory_properties = instance_state - .ash_instance - .get_physical_device_memory_properties(device_state.physical_device); - let memory_type_index = match find_memory_type( - &memory_properties, - requirements.memory_type_bits, - vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, - ) { - Some(index) => index, - None => { - device.destroy_buffer(buffer, None); - return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT); - } - }; - let allocate_info = vk::MemoryAllocateInfo { - allocation_size: requirements.size, - memory_type_index, - ..Default::default() - }; - let memory = match device.allocate_memory(&allocate_info, None) { - Ok(memory) => memory, - Err(error) => { - device.destroy_buffer(buffer, None); - return Err(error); - } - }; - if let Err(error) = device.bind_buffer_memory(buffer, memory, 0) { - device.free_memory(memory, None); - device.destroy_buffer(buffer, None); - return Err(error); - } - - let pool_info = vk::CommandPoolCreateInfo { - flags: vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, - queue_family_index, - ..Default::default() - }; - let command_pool = match device.create_command_pool(&pool_info, None) { - Ok(pool) => pool, - Err(error) => { - device.free_memory(memory, None); - device.destroy_buffer(buffer, None); - return Err(error); - } - }; - let alloc_info = vk::CommandBufferAllocateInfo { - command_pool, - level: vk::CommandBufferLevel::PRIMARY, - command_buffer_count: 1, - ..Default::default() - }; - let command_buffer = match device.allocate_command_buffers(&alloc_info) { - Ok(mut buffers) => buffers.remove(0), - Err(error) => { - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - device.destroy_buffer(buffer, None); - return Err(error); - } - }; - let fence = match device.create_fence(&vk::FenceCreateInfo::default(), None) { - Ok(fence) => fence, - Err(error) => { - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - device.destroy_buffer(buffer, None); - return Err(error); - } - }; - let present_wait_semaphore = - match device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) { - Ok(semaphore) => semaphore, - Err(error) => { - device.destroy_fence(fence, None); - device.destroy_command_pool(command_pool, None); - device.free_memory(memory, None); - device.destroy_buffer(buffer, None); - return Err(error); - } - }; - - Ok(ReadbackResources { - width, - height, - format_mode, - buffer, - memory, - command_pool, - command_buffer, - fence, - present_wait_semaphore, - size, - }) -} - -unsafe fn ensure_readback_resources<'a>( - device_state: &LayerDevice, - queue_state: &'a QueueState, - width: u32, - height: u32, - format_mode: FormatMode, -) -> Result>, vk::Result> { - let mut guard = queue_state - .resources - .lock() - .map_err(|_| vk::Result::ERROR_INITIALIZATION_FAILED)?; - let recreate = guard - .as_ref() - .map(|resources| { - resources.width != width - || resources.height != height - || resources.format_mode != format_mode - }) - .unwrap_or(true); - if recreate { - if let Some(mut old) = guard.take() { - destroy_readback_resources(&device_state.ash_device, &mut old); - } - *guard = Some(create_readback_resources( - device_state, - queue_state.family_index, - width, - height, - format_mode, - )?); - } - Ok(guard) -} - -unsafe fn capture_swapchain_image( - device_state: &LayerDevice, - queue_state: &QueueState, - queue: vk::Queue, - swapchain: &SwapchainState, - image_index: u32, - present_info: &vk::PresentInfoKHR, - ipc: &mut IpcState, - fallback_reason: u32, -) -> Result { - if !swapchain.can_capture || !queue_state.supports_transfer { - return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT); - } - let Some(format_mode) = format_mode(swapchain.format) else { - return Err(vk::Result::ERROR_FORMAT_NOT_SUPPORTED); - }; - let Some(&image) = swapchain.images.get(image_index as usize) else { - return Err(vk::Result::ERROR_OUT_OF_DATE_KHR); - }; - let width = swapchain.extent.width; - let height = swapchain.extent.height; - if width == 0 || height == 0 { - return Err(vk::Result::ERROR_OUT_OF_DATE_KHR); - } - - let mut resources_guard = - ensure_readback_resources(device_state, queue_state, width, height, format_mode)?; - let resources = resources_guard - .as_mut() - .ok_or(vk::Result::ERROR_INITIALIZATION_FAILED)?; - let device = &device_state.ash_device; - - device.reset_fences(&[resources.fence])?; - device.reset_command_pool(resources.command_pool, vk::CommandPoolResetFlags::empty())?; - let begin_info = vk::CommandBufferBeginInfo { - flags: vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT, - ..Default::default() - }; - device.begin_command_buffer(resources.command_buffer, &begin_info)?; - - let range = vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }; - let present_layout = swapchain.present_layout; - let shared_present = present_layout == vk::ImageLayout::SHARED_PRESENT_KHR; - let src_copy_layout = if shared_present { - vk::ImageLayout::SHARED_PRESENT_KHR - } else { - vk::ImageLayout::TRANSFER_SRC_OPTIMAL - }; - if !shared_present { - let to_transfer = vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::MEMORY_READ, - dst_access_mask: vk::AccessFlags::TRANSFER_READ, - old_layout: present_layout, - new_layout: vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - image, - subresource_range: range, - ..Default::default() - }; - device.cmd_pipeline_barrier( - resources.command_buffer, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::PipelineStageFlags::TRANSFER, - vk::DependencyFlags::empty(), - &[], - &[], - &[to_transfer], - ); - } - let copy = vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: 0, - buffer_image_height: 0, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - image_extent: vk::Extent3D { - width, - height, - depth: 1, - }, - }; - device.cmd_copy_image_to_buffer( - resources.command_buffer, - image, - src_copy_layout, - resources.buffer, - &[copy], - ); - if !shared_present { - let to_present = vk::ImageMemoryBarrier { - src_access_mask: vk::AccessFlags::TRANSFER_READ, - dst_access_mask: vk::AccessFlags::MEMORY_READ, - old_layout: vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - new_layout: present_layout, - src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - image, - subresource_range: range, - ..Default::default() - }; - device.cmd_pipeline_barrier( - resources.command_buffer, - vk::PipelineStageFlags::TRANSFER, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::DependencyFlags::empty(), - &[], - &[], - &[to_present], - ); - } - device.end_command_buffer(resources.command_buffer)?; - - let chained_wait = submit_capture( - device, - queue, - resources.command_buffer, - resources.fence, - resources.present_wait_semaphore, - present_info, - )?; - - if device - .wait_for_fences(&[resources.fence], true, CAPTURE_FENCE_TIMEOUT_NS) - .is_err() - { - return Ok(CaptureSubmit { chained_wait }); - } - - let mapped = match device.map_memory( - resources.memory, - 0, - resources.size, - vk::MemoryMapFlags::empty(), - ) { - Ok(mapped) => mapped, - Err(_) => return Ok(CaptureSubmit { chained_wait }), - }; - let _ = write_rows_to_shared_memory( - ipc, - swapchain.hwnd, - width, - height, - mapped.cast(), - (width * 4) as usize, - resources.format_mode, - fallback_reason, - ); - device.unmap_memory(resources.memory); - Ok(CaptureSubmit { chained_wait }) -} - -unsafe fn capture_swapchain_for_present( - device_state: &LayerDevice, - queue_state: &QueueState, - queue: vk::Queue, - swapchain: &SwapchainState, - image_index: u32, - present_info: &vk::PresentInfoKHR, - ipc: &mut IpcState, -) -> Result { - if !target_matches(ipc, swapchain.hwnd) || !frame_due(ipc) { - return Err(vk::Result::NOT_READY); - } - - let hdr_flags = hdr_format_flags(swapchain.format); - let fast_capturable = interop_format(swapchain.format).is_some(); - let cpu_capturable = format_mode(swapchain.format).is_some(); - - if !fast_capturable && !cpu_capturable { - let info = &mut *ipc.info; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.capture_flags = hdr_flags; - info.fallback_reason = GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED; - info.dropped_frame_counter = info.dropped_frame_counter.wrapping_add(1); - return Err(vk::Result::ERROR_FORMAT_NOT_SUPPORTED); - } - - let mode = capture_mode(); - let fast_path_possible = device_state.external_memory_enabled - && fast_capturable - && swapchain.shared_texture_allowed - && !fast_path_permanently_disabled(); - vlog!( - "present: mode={:?} fast_path_possible={} (ext_enabled={}, interop_format={:?}, shared_allowed={}, perm_disabled={}) swapchain_format={:?}", - mode, - fast_path_possible, - device_state.external_memory_enabled, - interop_format(swapchain.format).map(|f| f.dxgi_format.0), - swapchain.shared_texture_allowed, - fast_path_permanently_disabled(), - swapchain.format - ); - - if mode != CaptureMode::ForceCpu && fast_path_possible { - match capture_swapchain_image_shared( - device_state, - queue_state, - queue, - swapchain, - image_index, - present_info, - ipc, - ) { - Ok(result) => return Ok(result), - Err(error) => { - vlog!("fast-path shared-texture FAILED: {:?}", error); - if matches!( - error, - vk::Result::ERROR_INITIALIZATION_FAILED - | vk::Result::ERROR_EXTENSION_NOT_PRESENT - | vk::Result::ERROR_OUT_OF_DEVICE_MEMORY - | vk::Result::ERROR_FORMAT_NOT_SUPPORTED - | vk::Result::ERROR_FEATURE_NOT_PRESENT - | vk::Result::ERROR_INVALID_EXTERNAL_HANDLE - ) { - disable_fast_path(); - } - if mode == CaptureMode::ForceSharedTexture { - let info = &mut *ipc.info; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.fallback_reason = GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED; - info.state = GAME_CAPTURE_STATE_ERROR; - info.last_error = 70; - info.dropped_frame_counter = info.dropped_frame_counter.wrapping_add(1); - return Err(error); - } - } - } - } else if mode == CaptureMode::ForceSharedTexture { - let info = &mut *ipc.info; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.fallback_reason = if interop_format(swapchain.format).is_none() { - GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED - } else { - GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED - }; - info.state = GAME_CAPTURE_STATE_ERROR; - info.last_error = 70; - info.dropped_frame_counter = info.dropped_frame_counter.wrapping_add(1); - return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT); - } - - if !cpu_capturable { - let info = &mut *ipc.info; - info.api_type = GAME_CAPTURE_API_VULKAN; - info.capture_flags = hdr_flags; - info.fallback_reason = if mode == CaptureMode::ForceCpu { - GAME_CAPTURE_FALLBACK_FORCED_CPU - } else { - GAME_CAPTURE_FALLBACK_FORMAT_UNSUPPORTED - }; - info.dropped_frame_counter = info.dropped_frame_counter.wrapping_add(1); - return Err(vk::Result::ERROR_FORMAT_NOT_SUPPORTED); - } - - let fallback_reason = if mode == CaptureMode::ForceCpu { - GAME_CAPTURE_FALLBACK_FORCED_CPU - } else { - GAME_CAPTURE_FALLBACK_EXTERNAL_MEMORY_UNSUPPORTED - }; - capture_swapchain_image( - device_state, - queue_state, - queue, - swapchain, - image_index, - present_info, - ipc, - fallback_reason, - ) -} - -fn fast_path_permanently_disabled() -> bool { - FAST_PATH_DISABLED - .lock() - .map(|guard| *guard) - .unwrap_or(false) -} - -fn disable_fast_path() { - if let Ok(mut guard) = FAST_PATH_DISABLED.lock() { - *guard = true; - } -} - -unsafe fn try_capture_present( - queue: vk::Queue, - present_info: &vk::PresentInfoKHR, -) -> CaptureSubmit { - if present_info.swapchain_count == 0 - || present_info.p_swapchains.is_null() - || present_info.p_image_indices.is_null() - { - return CaptureSubmit::none(); - } - let Some(queue_state_ref) = QUEUE_MAP.get(&queue) else { - return CaptureSubmit::none(); - }; - let Some(device_state_ref) = DEVICE_MAP.get(&queue_state_ref.device) else { - return CaptureSubmit::none(); - }; - - let mut ipc_guard = match IPC_STATE.lock() { - Ok(guard) => guard, - Err(_) => { - return CaptureSubmit::none(); - } - }; - if ipc_guard.is_none() { - *ipc_guard = open_ipc(); - } - let Some(ipc) = ipc_guard.as_mut() else { - return CaptureSubmit::none(); - }; - if !capture_should_run(ipc) { - if let Some(mut state) = ipc_guard.take() { - free_ipc_state(&mut state); - } - return CaptureSubmit::none(); - } - - if host_supports_present_clock((*ipc.info).version) { - (*ipc.info).present_clock = GAME_CAPTURE_PRESENT_CLOCK_QPC; - (*ipc.info).last_present_timestamp_us = qpc_now_us(); - } else { - (*ipc.info).last_present_timestamp_us = now_us(); - } - - let swapchains = std::slice::from_raw_parts( - present_info.p_swapchains, - present_info.swapchain_count as usize, - ); - let indices = std::slice::from_raw_parts( - present_info.p_image_indices, - present_info.swapchain_count as usize, - ); - for (swapchain_handle, image_index) in swapchains.iter().zip(indices.iter()) { - let Some(swapchain_ref) = SWAPCHAIN_MAP.get(swapchain_handle) else { - continue; - }; - if swapchain_ref.device != queue_state_ref.device { - continue; - } - if let Ok(result) = capture_swapchain_for_present( - &device_state_ref, - &queue_state_ref, - queue, - &swapchain_ref, - *image_index, - present_info, - ipc, - ) { - return result; - } - } - CaptureSubmit::none() -} - -unsafe fn record_queue(device: vk::Device, queue: vk::Queue, family_index: u32) { - if queue == vk::Queue::null() { - return; - } - let supports_transfer = DEVICE_MAP - .get(&device) - .and_then(|device_state| { - device_state - .queue_families - .get(family_index as usize) - .copied() - }) - .map(|properties| { - properties.queue_flags.intersects( - vk::QueueFlags::GRAPHICS | vk::QueueFlags::COMPUTE | vk::QueueFlags::TRANSFER, - ) - }) - .unwrap_or(true); - QUEUE_MAP.insert( - queue, - QueueState { - device, - family_index, - supports_transfer, - resources: Mutex::new(None), - shared: Mutex::new(None), - }, - ); -} - -unsafe fn destroy_device_state(device: vk::Device) -> Option { - let device_state = DEVICE_MAP.remove(&device).map(|(_, value)| value)?; - let queue_keys: Vec = QUEUE_MAP - .iter() - .filter_map(|entry| (entry.value().device == device).then_some(*entry.key())) - .collect(); - for queue in queue_keys { - if let Some((_, queue_state)) = QUEUE_MAP.remove(&queue) { - if let Ok(mut resources) = queue_state.resources.lock() { - if let Some(mut resources) = resources.take() { - destroy_readback_resources(&device_state.ash_device, &mut resources); - } - } - if let Ok(mut shared) = queue_state.shared.lock() { - if let Some(mut shared) = shared.take() { - destroy_shared_texture_resources(&device_state.ash_device, &mut shared); - } - } - } - } - let swapchain_keys: Vec = SWAPCHAIN_MAP - .iter() - .filter_map(|entry| (entry.value().device == device).then_some(*entry.key())) - .collect(); - for swapchain in swapchain_keys { - SWAPCHAIN_MAP.remove(&swapchain); - } - GDPA_MAP.remove(&device); - Some(device_state) -} - -unsafe extern "system" fn dispatch_next_vkGetInstanceProcAddr( - instance: vk::Instance, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - if p_name.is_null() { - return None; - } - let name = CStr::from_ptr(p_name).to_bytes(); - let pfn: *const () = match name { - b"vkGetInstanceProcAddr" => dispatch_next_vkGetInstanceProcAddr as _, - b"vkGetDeviceProcAddr" => dispatch_next_vkGetDeviceProcAddr as _, - b"vkEnumerateInstanceExtensionProperties" - | b"vkEnumerateInstanceLayerProperties" - | b"vkEnumerateInstanceVersion" => return None, - _ => { - let gipa = GIPA.get()?; - return gipa(instance, p_name); - } - }; - Some(mem::transmute(pfn)) -} - -unsafe extern "system" fn dispatch_next_vkGetDeviceProcAddr( - device: vk::Device, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - if p_name.is_null() { - return None; - } - let name = CStr::from_ptr(p_name).to_bytes(); - if name == b"vkGetDeviceProcAddr" { - return Some(mem::transmute( - dispatch_next_vkGetDeviceProcAddr as *const (), - )); - } - let gdpa = GDPA_MAP.get(&device)?; - gdpa(device, p_name) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn vkNegotiateLoaderLayerInterfaceVersion( - p_version_struct: *mut NegotiateLayerInterface, -) -> vk::Result { - if p_version_struct.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let version_struct = &mut *p_version_struct; - version_struct.loader_layer_interface_version = - version_struct.loader_layer_interface_version.min(2); - version_struct.pfn_get_instance_proc_addr = Some(flux_vkGetInstanceProcAddr); - version_struct.pfn_get_device_proc_addr = Some(flux_vkGetDeviceProcAddr); - version_struct.pfn_get_physical_device_proc_addr = Some(flux_vk_layerGetPhysicalDeviceProcAddr); - vk::Result::SUCCESS -} -const _: PFN_vkNegotiateLoaderLayerInterfaceVersion = vkNegotiateLoaderLayerInterfaceVersion; - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn vkGetInstanceProcAddr( - instance: vk::Instance, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - flux_vkGetInstanceProcAddr(instance, p_name) -} - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkGetInstanceProcAddr( - instance: vk::Instance, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - if p_name.is_null() { - return None; - } - let name = CStr::from_ptr(p_name).to_bytes(); - let pfn: *const () = match name { - b"vkGetInstanceProcAddr" => flux_vkGetInstanceProcAddr as _, - b"vkGetDeviceProcAddr" => flux_vkGetDeviceProcAddr as _, - b"vkCreateInstance" => flux_vkCreateInstance as _, - b"vkDestroyInstance" => flux_vkDestroyInstance as _, - b"vkCreateDevice" => flux_vkCreateDevice as _, - b"vkDestroyDevice" => flux_vkDestroyDevice as _, - b"vkCreateWin32SurfaceKHR" => flux_vkCreateWin32SurfaceKHR as _, - b"vkDestroySurfaceKHR" => flux_vkDestroySurfaceKHR as _, - b"vk_layerGetPhysicalDeviceProcAddr" => flux_vk_layerGetPhysicalDeviceProcAddr as _, - _ => { - let gipa = GIPA.get()?; - return gipa(instance, p_name); - } - }; - Some(mem::transmute(pfn)) -} -const _: vk::PFN_vkGetInstanceProcAddr = flux_vkGetInstanceProcAddr; - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn vkGetDeviceProcAddr( - device: vk::Device, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - flux_vkGetDeviceProcAddr(device, p_name) -} - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkGetDeviceProcAddr( - device: vk::Device, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - if p_name.is_null() { - return None; - } - let name = CStr::from_ptr(p_name).to_bytes(); - let pfn: *const () = match name { - b"vkGetDeviceProcAddr" => flux_vkGetDeviceProcAddr as _, - b"vkCreateDevice" => flux_vkCreateDevice as _, - b"vkDestroyDevice" => flux_vkDestroyDevice as _, - b"vkCreateSwapchainKHR" => flux_vkCreateSwapchainKHR as _, - b"vkDestroySwapchainKHR" => flux_vkDestroySwapchainKHR as _, - b"vkGetSwapchainImagesKHR" => flux_vkGetSwapchainImagesKHR as _, - b"vkQueuePresentKHR" => flux_vkQueuePresentKHR as _, - b"vkGetDeviceQueue" => flux_vkGetDeviceQueue as _, - b"vkGetDeviceQueue2" => flux_vkGetDeviceQueue2 as _, - _ => { - let gdpa = GDPA_MAP.get(&device)?; - return gdpa(device, p_name); - } - }; - Some(mem::transmute(pfn)) -} -const _: vk::PFN_vkGetDeviceProcAddr = flux_vkGetDeviceProcAddr; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vk_layerGetPhysicalDeviceProcAddr( - instance: vk::Instance, - p_name: *const c_char, -) -> vk::PFN_vkVoidFunction { - if p_name.is_null() { - return None; - } - let name = CStr::from_ptr(p_name).to_bytes(); - if name == b"vkCreateDevice" { - return Some(mem::transmute(flux_vkCreateDevice as *const ())); - } - let gphypa = GPHYPA.get()?; - gphypa(instance, p_name) -} -const _: PFN_vk_layerGetPhysicalDeviceProcAddr = flux_vk_layerGetPhysicalDeviceProcAddr; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkCreateInstance( - p_create_info: *const vk::InstanceCreateInfo, - p_allocator: *const vk::AllocationCallbacks, - p_instance: *mut vk::Instance, -) -> vk::Result { - if p_create_info.is_null() || p_instance.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let create_info = p_create_info.read(); - let Some(mut chain_info) = - get_instance_chain_info(&create_info, LayerFunction::LAYER_LINK_INFO) - else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let chain_info = chain_info.as_mut(); - let layer_info = chain_info.u.p_layer_info.read(); - chain_info.u.p_layer_info = layer_info.p_next; - - let Some(gipa) = layer_info.pfn_next_get_instance_proc_addr else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - if let Some(gphypa) = layer_info.pfn_next_get_physical_device_proc_addr { - let _ = GPHYPA.set(gphypa); - } - let create_name = CStr::from_bytes_with_nul_unchecked(b"vkCreateInstance\0"); - let Some(create_instance_ptr) = gipa(vk::Instance::null(), create_name.as_ptr()) else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let create_instance: vk::PFN_vkCreateInstance = mem::transmute(create_instance_ptr); - - let augmented = AugmentedExtensions::build( - create_info.pp_enabled_extension_names, - create_info.enabled_extension_count, - &[ - INSTANCE_EXT_PHYSICAL_DEVICE_PROPERTIES2, - INSTANCE_EXT_EXTERNAL_MEMORY_CAPABILITIES, - ], - ); - let mut augmented_info = create_info; - augmented_info.enabled_extension_count = augmented.count(); - augmented_info.pp_enabled_extension_names = augmented.as_ptr(); - - let result = if capture_mode() != CaptureMode::ForceCpu { - let augmented_result = create_instance(&augmented_info, p_allocator, p_instance); - if augmented_result == vk::Result::SUCCESS { - augmented_result - } else { - create_instance(p_create_info, p_allocator, p_instance) - } - } else { - create_instance(p_create_info, p_allocator, p_instance) - }; - if result != vk::Result::SUCCESS { - return result; - } - - let instance = *p_instance; - let _ = GIPA.set(gipa); - let entry = ash::Entry::from_static_fn(ash::StaticFn { - get_instance_proc_addr: dispatch_next_vkGetInstanceProcAddr, - }); - let _ = ENTRY.set(entry.clone()); - let ash_instance = ash::Instance::load(entry.static_fn(), instance); - let khr_surface = khr::surface::Instance::new(&entry, &ash_instance); - let khr_win32_surface = khr::win32_surface::Instance::new(&entry, &ash_instance); - if let Ok(physical_devices) = ash_instance.enumerate_physical_devices() { - for physical_device in physical_devices { - PHY_TO_INSTANCE_MAP.insert(physical_device, instance); - } - } - INSTANCE_MAP.insert( - instance, - LayerInstance { - ash_instance, - khr_surface, - khr_win32_surface, - }, - ); - vk::Result::SUCCESS -} -const _: vk::PFN_vkCreateInstance = flux_vkCreateInstance; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkDestroyInstance( - instance: vk::Instance, - p_allocator: *const vk::AllocationCallbacks, -) { - let Some((_, instance_state)) = INSTANCE_MAP.remove(&instance) else { - return; - }; - if let Ok(physical_devices) = instance_state.ash_instance.enumerate_physical_devices() { - for physical_device in physical_devices { - PHY_TO_INSTANCE_MAP.remove(&physical_device); - } - } - let surfaces: Vec = SURFACE_MAP - .iter() - .filter_map(|entry| (entry.value().instance == instance).then_some(*entry.key())) - .collect(); - for surface in surfaces { - SURFACE_MAP.remove(&surface); - } - (instance_state.ash_instance.fp_v1_0().destroy_instance)(instance, p_allocator); -} -const _: vk::PFN_vkDestroyInstance = flux_vkDestroyInstance; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkCreateDevice( - physical_device: vk::PhysicalDevice, - p_create_info: *const vk::DeviceCreateInfo, - p_allocator: *const vk::AllocationCallbacks, - p_device: *mut vk::Device, -) -> vk::Result { - if p_create_info.is_null() || p_device.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let instance = PHY_TO_INSTANCE_MAP - .get(&physical_device) - .map(|entry| *entry) - .or_else(|| INSTANCE_MAP.iter().next().map(|entry| *entry.key())); - let Some(instance) = instance else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let Some(instance_state) = INSTANCE_MAP.get(&instance) else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let instance_fn = instance_state.ash_instance.fp_v1_0(); - - let create_info = p_create_info.read(); - let Some(mut chain_info) = get_device_chain_info(&create_info, LayerFunction::LAYER_LINK_INFO) - else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let chain_info = chain_info.as_mut(); - let layer_info = chain_info.u.p_layer_info.read(); - chain_info.u.p_layer_info = layer_info.p_next; - - let Some(gdpa) = layer_info.pfn_next_get_device_proc_addr else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - - let want_external_memory = capture_mode() != CaptureMode::ForceCpu; - let augmented = AugmentedExtensions::build( - create_info.pp_enabled_extension_names, - create_info.enabled_extension_count, - &[ - DEVICE_EXT_EXTERNAL_MEMORY, - DEVICE_EXT_EXTERNAL_MEMORY_WIN32, - DEVICE_EXT_DEDICATED_ALLOCATION, - DEVICE_EXT_GET_MEMORY_REQUIREMENTS2, - ], - ); - let mut augmented_info = create_info; - augmented_info.enabled_extension_count = augmented.count(); - augmented_info.pp_enabled_extension_names = augmented.as_ptr(); - - let mut external_memory_enabled = false; - let result = if want_external_memory { - let augmented_result = - (instance_fn.create_device)(physical_device, &augmented_info, p_allocator, p_device); - if augmented_result == vk::Result::SUCCESS { - external_memory_enabled = true; - augmented_result - } else { - (instance_fn.create_device)(physical_device, p_create_info, p_allocator, p_device) - } - } else { - (instance_fn.create_device)(physical_device, p_create_info, p_allocator, p_device) - }; - if result != vk::Result::SUCCESS { - return result; - } - let app_external_memory_win32 = extension_in_list( - create_info.pp_enabled_extension_names, - create_info.enabled_extension_count, - DEVICE_EXT_EXTERNAL_MEMORY_WIN32, - ); - let external_memory_enabled = external_memory_enabled || app_external_memory_win32; - vlog!( - "vkCreateDevice: external_memory_enabled={external_memory_enabled} (want={want_external_memory}, app_enabled={app_external_memory_win32})" - ); - let dedicated_allocation_enabled = external_memory_enabled - || extension_in_list( - create_info.pp_enabled_extension_names, - create_info.enabled_extension_count, - DEVICE_EXT_DEDICATED_ALLOCATION, - ); - - let device = *p_device; - GDPA_MAP.insert(device, gdpa); - let ash_device = ash::Device::load(instance_fn, device); - let khr_swapchain = khr::swapchain::Device::new(&instance_state.ash_instance, &ash_device); - let khr_external_memory_win32 = external_memory_enabled.then(|| { - khr::external_memory_win32::Device::new(&instance_state.ash_instance, &ash_device) - }); - let queue_families = instance_state - .ash_instance - .get_physical_device_queue_family_properties(physical_device); - DEVICE_MAP.insert( - device, - LayerDevice { - instance, - physical_device, - ash_device: ash_device.clone(), - khr_swapchain, - queue_families, - khr_external_memory_win32, - external_memory_enabled, - dedicated_allocation_enabled, - }, - ); - - if !create_info.p_queue_create_infos.is_null() { - let queue_infos = std::slice::from_raw_parts( - create_info.p_queue_create_infos, - create_info.queue_create_info_count as usize, - ); - for queue_info in queue_infos { - for queue_index in 0..queue_info.queue_count { - let mut queue = vk::Queue::null(); - (ash_device.fp_v1_0().get_device_queue)( - device, - queue_info.queue_family_index, - queue_index, - &mut queue, - ); - record_queue(device, queue, queue_info.queue_family_index); - } - } - } - - vk::Result::SUCCESS -} -const _: vk::PFN_vkCreateDevice = flux_vkCreateDevice; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkDestroyDevice( - device: vk::Device, - p_allocator: *const vk::AllocationCallbacks, -) { - let Some(device_state) = destroy_device_state(device) else { - return; - }; - (device_state.ash_device.fp_v1_0().destroy_device)(device, p_allocator); -} -const _: vk::PFN_vkDestroyDevice = flux_vkDestroyDevice; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkCreateWin32SurfaceKHR( - instance: vk::Instance, - p_create_info: *const vk::Win32SurfaceCreateInfoKHR, - p_allocator: *const vk::AllocationCallbacks, - p_surface: *mut vk::SurfaceKHR, -) -> vk::Result { - let Some(instance_state) = INSTANCE_MAP.get(&instance) else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let result = (instance_state - .khr_win32_surface - .fp() - .create_win32_surface_khr)(instance, p_create_info, p_allocator, p_surface); - if result == vk::Result::SUCCESS && !p_surface.is_null() && !p_create_info.is_null() { - let surface = *p_surface; - SURFACE_MAP.insert( - surface, - SurfaceState { - instance, - hwnd: (*p_create_info).hwnd, - }, - ); - } - result -} -const _: vk::PFN_vkCreateWin32SurfaceKHR = flux_vkCreateWin32SurfaceKHR; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkDestroySurfaceKHR( - instance: vk::Instance, - surface: vk::SurfaceKHR, - p_allocator: *const vk::AllocationCallbacks, -) { - SURFACE_MAP.remove(&surface); - if let Some(instance_state) = INSTANCE_MAP.get(&instance) { - (instance_state.khr_surface.fp().destroy_surface_khr)(instance, surface, p_allocator); - } -} -const _: vk::PFN_vkDestroySurfaceKHR = flux_vkDestroySurfaceKHR; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkCreateSwapchainKHR( - device: vk::Device, - p_create_info: *const vk::SwapchainCreateInfoKHR, - p_allocator: *const vk::AllocationCallbacks, - p_swapchain: *mut vk::SwapchainKHR, -) -> vk::Result { - if p_create_info.is_null() || p_swapchain.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let Some(device_state) = DEVICE_MAP.get(&device) else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let original_info = p_create_info.read(); - let original_can_capture = original_info - .image_usage - .contains(vk::ImageUsageFlags::TRANSFER_SRC); - let create_info = swapchain_create_info_with_transfer_src(original_info); - let mut used_transfer_src = true; - let mut result = (device_state.khr_swapchain.fp().create_swapchain_khr)( - device, - &create_info, - p_allocator, - p_swapchain, - ); - if result != vk::Result::SUCCESS { - used_transfer_src = original_can_capture; - let retry_info = swapchain_retry_create_info(original_info); - result = (device_state.khr_swapchain.fp().create_swapchain_khr)( - device, - &retry_info, - p_allocator, - p_swapchain, - ); - } - if result != vk::Result::SUCCESS { - return result; - } - - if let Some(old_swapchain) = - old_swapchain_to_remove_after_create(result, original_info.old_swapchain) - { - SWAPCHAIN_MAP.remove(&old_swapchain); - } - let swapchain = *p_swapchain; - let mut count = 0u32; - let get_result = (device_state.khr_swapchain.fp().get_swapchain_images_khr)( - device, - swapchain, - &mut count, - null_mut(), - ); - let mut images = Vec::new(); - if get_result == vk::Result::SUCCESS && count > 0 { - images.resize(count as usize, vk::Image::null()); - let image_result = (device_state.khr_swapchain.fp().get_swapchain_images_khr)( - device, - swapchain, - &mut count, - images.as_mut_ptr(), - ); - if image_result == vk::Result::SUCCESS || image_result == vk::Result::INCOMPLETE { - images.truncate(count as usize); - } else { - images.clear(); - } - } - let hwnd = SURFACE_MAP - .get(&original_info.surface) - .map(|surface| surface.hwnd) - .unwrap_or(0); - let present_layout = present_layout_for_mode(original_info.present_mode); - let shared_texture_allowed = - shared_texture_allowed_for_present_mode(original_info.present_mode); - let have_images = !images.is_empty(); - SWAPCHAIN_MAP.insert( - swapchain, - SwapchainState { - device, - hwnd, - extent: original_info.image_extent, - format: original_info.image_format, - images, - can_capture: used_transfer_src && hwnd != 0 && have_images, - present_layout, - shared_texture_allowed, - }, - ); - vk::Result::SUCCESS -} -const _: vk::PFN_vkCreateSwapchainKHR = flux_vkCreateSwapchainKHR; - -fn swapchain_create_info_with_transfer_src( - mut create_info: vk::SwapchainCreateInfoKHR, -) -> vk::SwapchainCreateInfoKHR { - create_info.image_usage |= vk::ImageUsageFlags::TRANSFER_SRC; - create_info -} - -fn swapchain_retry_create_info( - original_info: vk::SwapchainCreateInfoKHR, -) -> vk::SwapchainCreateInfoKHR { - original_info -} - -fn old_swapchain_to_remove_after_create( - result: vk::Result, - old_swapchain: vk::SwapchainKHR, -) -> Option { - (result == vk::Result::SUCCESS && old_swapchain != vk::SwapchainKHR::null()) - .then_some(old_swapchain) -} - -fn present_layout_for_mode(present_mode: vk::PresentModeKHR) -> vk::ImageLayout { - match present_mode { - vk::PresentModeKHR::SHARED_DEMAND_REFRESH - | vk::PresentModeKHR::SHARED_CONTINUOUS_REFRESH => vk::ImageLayout::SHARED_PRESENT_KHR, - _ => vk::ImageLayout::PRESENT_SRC_KHR, - } -} - -fn shared_texture_allowed_for_present_mode(present_mode: vk::PresentModeKHR) -> bool { - present_mode != vk::PresentModeKHR::IMMEDIATE -} - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkDestroySwapchainKHR( - device: vk::Device, - swapchain: vk::SwapchainKHR, - p_allocator: *const vk::AllocationCallbacks, -) { - SWAPCHAIN_MAP.remove(&swapchain); - if let Some(device_state) = DEVICE_MAP.get(&device) { - (device_state.khr_swapchain.fp().destroy_swapchain_khr)(device, swapchain, p_allocator); - } -} -const _: vk::PFN_vkDestroySwapchainKHR = flux_vkDestroySwapchainKHR; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkGetSwapchainImagesKHR( - device: vk::Device, - swapchain: vk::SwapchainKHR, - p_swapchain_image_count: *mut u32, - p_swapchain_images: *mut vk::Image, -) -> vk::Result { - let Some(device_state) = DEVICE_MAP.get(&device) else { - return vk::Result::ERROR_INITIALIZATION_FAILED; - }; - let result = (device_state.khr_swapchain.fp().get_swapchain_images_khr)( - device, - swapchain, - p_swapchain_image_count, - p_swapchain_images, - ); - if (result == vk::Result::SUCCESS || result == vk::Result::INCOMPLETE) - && !p_swapchain_image_count.is_null() - && !p_swapchain_images.is_null() - { - let count = *p_swapchain_image_count as usize; - if let Some(mut swapchain_state) = SWAPCHAIN_MAP.get_mut(&swapchain) { - swapchain_state.images = std::slice::from_raw_parts(p_swapchain_images, count).to_vec(); - } - } - result -} -const _: vk::PFN_vkGetSwapchainImagesKHR = flux_vkGetSwapchainImagesKHR; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkQueuePresentKHR( - queue: vk::Queue, - p_present_info: *const vk::PresentInfoKHR, -) -> vk::Result { - if p_present_info.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let present_info = *p_present_info; - let capture = try_capture_present(queue, &present_info); - let Some(queue_state) = QUEUE_MAP.get(&queue) else { - return vk::Result::ERROR_DEVICE_LOST; - }; - let Some(device_state) = DEVICE_MAP.get(&queue_state.device) else { - return vk::Result::ERROR_DEVICE_LOST; - }; - let chained_slot = capture.chained_wait; - if let Some(chained) = chained_slot { - let modified = present_info_with_chained_wait(present_info, &chained); - return (device_state.khr_swapchain.fp().queue_present_khr)(queue, &modified); - } - (device_state.khr_swapchain.fp().queue_present_khr)(queue, p_present_info) -} -const _: vk::PFN_vkQueuePresentKHR = flux_vkQueuePresentKHR; - -fn present_info_with_chained_wait<'a>( - mut present_info: vk::PresentInfoKHR<'a>, - chained: &'a vk::Semaphore, -) -> vk::PresentInfoKHR<'a> { - present_info.wait_semaphore_count = 1; - present_info.p_wait_semaphores = chained; - present_info -} - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkGetDeviceQueue( - device: vk::Device, - queue_family_index: u32, - queue_index: u32, - p_queue: *mut vk::Queue, -) { - if let Some(device_state) = DEVICE_MAP.get(&device) { - (device_state.ash_device.fp_v1_0().get_device_queue)( - device, - queue_family_index, - queue_index, - p_queue, - ); - if !p_queue.is_null() { - record_queue(device, *p_queue, queue_family_index); - } - } -} -const _: vk::PFN_vkGetDeviceQueue = flux_vkGetDeviceQueue; - -#[unsafe(no_mangle)] -unsafe extern "system" fn flux_vkGetDeviceQueue2( - device: vk::Device, - p_queue_info: *const vk::DeviceQueueInfo2, - p_queue: *mut vk::Queue, -) { - if let Some(device_state) = DEVICE_MAP.get(&device) { - (device_state.ash_device.fp_v1_1().get_device_queue2)(device, p_queue_info, p_queue); - if !p_queue_info.is_null() && !p_queue.is_null() { - record_queue(device, *p_queue, (*p_queue_info).queue_family_index); - } - } -} -const _: vk::PFN_vkGetDeviceQueue2 = flux_vkGetDeviceQueue2; - -#[cfg(test)] -mod tests { - use super::*; - use ash::vk::Handle; - - #[test] - fn format_mode_accepts_obs_capture_8bit_formats() { - assert_eq!( - format_mode(vk::Format::B8G8R8A8_UNORM), - Some(FormatMode::Bgra) - ); - assert_eq!( - format_mode(vk::Format::B8G8R8A8_SRGB), - Some(FormatMode::Bgra) - ); - assert_eq!( - format_mode(vk::Format::R8G8B8A8_UNORM), - Some(FormatMode::Rgba) - ); - assert_eq!( - format_mode(vk::Format::R8G8B8A8_SRGB), - Some(FormatMode::Rgba) - ); - assert_eq!(format_mode(vk::Format::A2B10G10R10_UNORM_PACK32), None); - } - - #[test] - fn hdr_format_flags_mark_ten_bit_and_float_formats() { - assert_eq!( - hdr_format_flags(vk::Format::A2B10G10R10_UNORM_PACK32), - GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR - ); - assert_eq!( - hdr_format_flags(vk::Format::A2R10G10B10_UNORM_PACK32), - GAME_CAPTURE_FLAG_TEN_BIT | GAME_CAPTURE_FLAG_HDR - ); - assert_eq!( - hdr_format_flags(vk::Format::R16G16B16A16_SFLOAT), - GAME_CAPTURE_FLAG_HDR - ); - assert_eq!(hdr_format_flags(vk::Format::B8G8R8A8_UNORM), 0); - } - - #[test] - fn present_layout_uses_shared_present_only_for_shared_modes() { - assert_eq!( - present_layout_for_mode(vk::PresentModeKHR::SHARED_DEMAND_REFRESH), - vk::ImageLayout::SHARED_PRESENT_KHR - ); - assert_eq!( - present_layout_for_mode(vk::PresentModeKHR::SHARED_CONTINUOUS_REFRESH), - vk::ImageLayout::SHARED_PRESENT_KHR - ); - assert_eq!( - present_layout_for_mode(vk::PresentModeKHR::FIFO), - vk::ImageLayout::PRESENT_SRC_KHR - ); - assert_eq!( - present_layout_for_mode(vk::PresentModeKHR::MAILBOX), - vk::ImageLayout::PRESENT_SRC_KHR - ); - } - - #[test] - fn immediate_present_mode_uses_cpu_fallback_after_real_shared_handle_failure() { - assert!(!shared_texture_allowed_for_present_mode( - vk::PresentModeKHR::IMMEDIATE - )); - assert!(shared_texture_allowed_for_present_mode( - vk::PresentModeKHR::FIFO - )); - assert!(shared_texture_allowed_for_present_mode( - vk::PresentModeKHR::MAILBOX - )); - } - - #[test] - fn shared_texture_reacquires_external_ownership_after_first_frame() { - let queue_family_index = 7; - assert_eq!( - shared_texture_dst_pre_queue_families(false, queue_family_index), - (vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED) - ); - assert_eq!( - shared_texture_dst_pre_queue_families(true, queue_family_index), - (vk::QUEUE_FAMILY_EXTERNAL, queue_family_index) - ); - } - - #[test] - fn augmented_swapchain_create_info_preserves_old_swapchain() { - let old = vk::SwapchainKHR::from_raw(0x55); - let original = vk::SwapchainCreateInfoKHR { - image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT, - old_swapchain: old, - ..Default::default() - }; - - let augmented = swapchain_create_info_with_transfer_src(original); - assert!( - augmented - .image_usage - .contains(vk::ImageUsageFlags::TRANSFER_SRC) - ); - assert!( - augmented - .image_usage - .contains(vk::ImageUsageFlags::COLOR_ATTACHMENT) - ); - assert_eq!(augmented.old_swapchain.as_raw(), old.as_raw()); - } - - #[test] - fn retry_swapchain_create_info_keeps_app_usage_and_old_swapchain() { - let old = vk::SwapchainKHR::from_raw(0x66); - let original = vk::SwapchainCreateInfoKHR { - image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT, - old_swapchain: old, - ..Default::default() - }; - - let retry = swapchain_retry_create_info(original); - assert_eq!(retry.image_usage, original.image_usage); - assert_eq!(retry.old_swapchain.as_raw(), old.as_raw()); - } - - #[test] - fn old_swapchain_is_removed_only_after_successful_recreate() { - let old = vk::SwapchainKHR::from_raw(0x77); - assert_eq!( - old_swapchain_to_remove_after_create(vk::Result::SUCCESS, old).map(|h| h.as_raw()), - Some(old.as_raw()) - ); - assert_eq!( - old_swapchain_to_remove_after_create(vk::Result::ERROR_INITIALIZATION_FAILED, old) - .map(|h| h.as_raw()), - None - ); - assert_eq!( - old_swapchain_to_remove_after_create(vk::Result::SUCCESS, vk::SwapchainKHR::null()) - .map(|h| h.as_raw()), - None - ); - } - - #[test] - fn present_wait_semaphore_slice_rejects_missing_pointer() { - let waits = [vk::Semaphore::from_raw(0x11), vk::Semaphore::from_raw(0x12)]; - let present_info = vk::PresentInfoKHR { - wait_semaphore_count: waits.len() as u32, - p_wait_semaphores: waits.as_ptr(), - ..Default::default() - }; - let slice = unsafe { present_wait_semaphores(&present_info) }; - assert_eq!( - slice.iter().map(|s| s.as_raw()).collect::>(), - vec![0x11, 0x12] - ); - - let missing_pointer = vk::PresentInfoKHR { - wait_semaphore_count: waits.len() as u32, - p_wait_semaphores: null(), - ..Default::default() - }; - assert!(unsafe { present_wait_semaphores(&missing_pointer) }.is_empty()); - } - - #[test] - fn chained_present_wait_replaces_wait_list_but_preserves_present_payload() { - let original_waits = [vk::Semaphore::from_raw(0x21), vk::Semaphore::from_raw(0x22)]; - let swapchains = [vk::SwapchainKHR::from_raw(0x31)]; - let image_indices = [3_u32]; - let mut results = [vk::Result::SUCCESS]; - let present_info = vk::PresentInfoKHR { - wait_semaphore_count: original_waits.len() as u32, - p_wait_semaphores: original_waits.as_ptr(), - swapchain_count: swapchains.len() as u32, - p_swapchains: swapchains.as_ptr(), - p_image_indices: image_indices.as_ptr(), - p_results: results.as_mut_ptr(), - ..Default::default() - }; - let chained = vk::Semaphore::from_raw(0x44); - - let modified = present_info_with_chained_wait(present_info, &chained); - - assert_eq!(modified.wait_semaphore_count, 1); - assert_eq!( - unsafe { *modified.p_wait_semaphores }.as_raw(), - chained.as_raw() - ); - assert_eq!(modified.swapchain_count, present_info.swapchain_count); - assert_eq!(modified.p_swapchains, present_info.p_swapchains); - assert_eq!(modified.p_image_indices, present_info.p_image_indices); - assert_eq!(modified.p_results, present_info.p_results); - } -} - -#[unsafe(no_mangle)] -unsafe extern "system" fn vkEnumerateInstanceLayerProperties( - p_property_count: *mut u32, - p_properties: *mut vk::LayerProperties, -) -> vk::Result { - if p_property_count.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - if p_properties.is_null() { - *p_property_count = 1; - return vk::Result::SUCCESS; - } - if *p_property_count == 0 { - return vk::Result::INCOMPLETE; - } - *p_property_count = 1; - let mut properties = vk::LayerProperties { - spec_version: vk::API_VERSION_1_0, - implementation_version: 1, - ..Default::default() - }; - let name = LAYER_NAME; - std::ptr::copy_nonoverlapping( - name.as_ptr() as *const c_char, - properties.layer_name.as_mut_ptr(), - name.len().min(properties.layer_name.len()), - ); - *p_properties = properties; - vk::Result::SUCCESS -} diff --git a/fluxer_desktop/src/common/Types.ts b/fluxer_desktop/src/common/Types.ts index 293988c8c..1187bbae9 100644 --- a/fluxer_desktop/src/common/Types.ts +++ b/fluxer_desktop/src/common/Types.ts @@ -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; diff --git a/fluxer_desktop/src/main/NativeScreenCapture.test.mjs b/fluxer_desktop/src/main/NativeScreenCapture.test.mjs index a2e2ca3b9..095b7d597 100644 --- a/fluxer_desktop/src/main/NativeScreenCapture.test.mjs +++ b/fluxer_desktop/src/main/NativeScreenCapture.test.mjs @@ -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'); diff --git a/fluxer_desktop/src/main/NativeScreenCapture.ts b/fluxer_desktop/src/main/NativeScreenCapture.ts index a7b226301..017f2230b 100644 --- a/fluxer_desktop/src/main/NativeScreenCapture.ts +++ b/fluxer_desktop/src/main/NativeScreenCapture.ts @@ -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, diff --git a/fluxer_desktop/src/main/NativeScreenCaptureValidation.ts b/fluxer_desktop/src/main/NativeScreenCaptureValidation.ts index 5e8019491..750f2846d 100644 --- a/fluxer_desktop/src/main/NativeScreenCaptureValidation.ts +++ b/fluxer_desktop/src/main/NativeScreenCaptureValidation.ts @@ -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' || diff --git a/fluxer_desktop/src/main/WindowsVulkanGameCaptureLayer.ts b/fluxer_desktop/src/main/WindowsVulkanGameCaptureLayer.ts deleted file mode 100644 index a06b942a5..000000000 --- a/fluxer_desktop/src/main/WindowsVulkanGameCaptureLayer.ts +++ /dev/null @@ -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 { - const valueNames: Array = []; - 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 { - 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; - 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); -} diff --git a/fluxer_desktop/src/main/WindowsVulkanLayerCleanup.ts b/fluxer_desktop/src/main/WindowsVulkanLayerCleanup.ts new file mode 100644 index 000000000..8e2ed1531 --- /dev/null +++ b/fluxer_desktop/src/main/WindowsVulkanLayerCleanup.ts @@ -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 { + const valueNames: Array = []; + 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 { + 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; + 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}); + } + } +} diff --git a/fluxer_desktop/src/main/index.ts b/fluxer_desktop/src/main/index.ts index 442e269fc..5e3d3a171 100644 --- a/fluxer_desktop/src/main/index.ts +++ b/fluxer_desktop/src/main/index.ts @@ -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); diff --git a/knip.json b/knip.json index 2c8dd5eff..ca4a6d2c2 100644 --- a/knip.json +++ b/knip.json @@ -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"] }, diff --git a/tools/ci/src/desktop_native.rs b/tools/ci/src/desktop_native.rs index 3290ac42e..6b0737c12 100644 --- a/tools/ci/src/desktop_native.rs +++ b/tools/ci/src/desktop_native.rs @@ -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 { + 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, 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" - ); - } }