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

This commit is contained in:
Hampus authored and GitHub committed 2026-08-29 14:26:28 +02:00
1 parent 9ad026b8ce
commit 9d0be1ebd1
43 files changed
+158 -12089

No files matched your search

+1 -6
View File
@@ -472,8 +472,6 @@ export interface NativeScreenCaptureSource {
targetPid?: number;
}
export type GameCaptureInjectionMethod = 'auto' | 'remote-thread' | 'set-windows-hook';
export interface NativeScreenCaptureRect {
x: number;
y: number;
@@ -487,7 +485,6 @@ export interface NativeScreenCaptureStartOptions {
width?: number;
height?: number;
frameRate?: number;
injectionMethod?: GameCaptureInjectionMethod;
captureId?: string;
colorRange?: 'full' | 'limited';
colorSpace?: 'rec709' | 'srgb';
@@ -523,7 +520,7 @@ export interface NativeScreenCaptureLifecycleMessage {
source?: NativeScreenCaptureLifecycleSource;
}
export type NativeScreenCaptureStrategy = 'game-hook' | 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export type NativeScreenCaptureStrategy = 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export interface NativeScreenCaptureDiagnostics {
state?: number;
@@ -538,8 +535,6 @@ export interface NativeScreenCaptureDiagnostics {
droppedFrameCounter?: number;
lastPresentTimestampUs?: number;
lastError?: number;
requestedInjectionMethod?: string;
injectionMethod?: string;
activeStrategy?: NativeScreenCaptureStrategy;
lastFallbackReason?: string;
backend?: string;
@@ -273,7 +273,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 2560,
height: 1440,
frameRate: 60,
injectionMethod: undefined,
captureId: 'capture-1',
colorRange: 'full',
colorSpace: 'rec709',
@@ -287,7 +286,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 1280,
height: 720,
frameRate: 30,
injectionMethod: undefined,
captureId: 'capture-2',
colorRange: undefined,
colorSpace: undefined,
@@ -352,7 +350,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 1280,
height: 720,
frameRate: 30,
injectionMethod: undefined,
captureId: 'preselected-capture-id',
colorRange: undefined,
colorSpace: undefined,
@@ -485,14 +482,12 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
width: 2560,
height: 1440,
frameRate: 60,
injectionMethod: 'set-windows-hook',
nativeFrameSinkRequired: true,
},
);
assert.equal(captures.length, 1);
assert.equal(captures[0].options.sourceKind, 'screen');
assert.equal(captures[0].options.injectionMethod, undefined);
const diagnostics = await harness.handlers.get('native-screen-capture:get-diagnostics')({sender}, result.captureId);
assert.equal(diagnostics.sourceKind, 'screen');
@@ -827,7 +827,6 @@ async function startNativeScreenCapture(
width: requestedWidth,
height: requestedHeight,
frameRate: options.frameRate ?? 30,
injectionMethod: options.sourceKind === 'game' ? options.injectionMethod : undefined,
captureId,
colorRange: options.colorRange,
colorSpace: options.colorSpace,
@@ -86,14 +86,6 @@ export function isValidStartOptions(options: unknown): options is NativeScreenCa
) {
return false;
}
if (
options.injectionMethod !== undefined &&
options.injectionMethod !== 'auto' &&
options.injectionMethod !== 'remote-thread' &&
options.injectionMethod !== 'set-windows-hook'
) {
return false;
}
if (
options.captureId !== undefined &&
(typeof options.captureId !== 'string' ||
@@ -1,165 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync} from 'node:child_process';
import {createRequire} from 'node:module';
import log from 'electron-log';
const requireModule = createRequire(import.meta.url);
const VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY = 'Software\\Khronos\\Vulkan\\ImplicitLayers';
const VULKAN_REGISTRY_ROOTS = ['HKCU', 'HKLM'] as const;
const FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME = /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/;
const FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES = new Set(['win-game-capture', 'win-screen-capture']);
interface VulkanLayerRegistrationState {
registered: boolean;
manifestExists: boolean;
dllExists: boolean;
manifestPath: string | null;
}
type WindowsGameCaptureModule = {
loadError?: Error | null;
isGameCaptureHookAvailable?: () => boolean;
registerVulkanLayerManifest?: () => boolean;
unregisterVulkanLayerManifest?: () => boolean;
resolveVulkanLayerManifestPath?: () => string | null;
getVulkanLayerRegistrationState?: () => VulkanLayerRegistrationState;
};
function parseRegistryValueNames(stdout: string): Array<string> {
const valueNames: Array<string> = [];
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
const match = trimmed.match(/^(.*?)\s+REG_DWORD\s+(?:0x[0-9a-f]+|\d+)$/i);
if (!match) continue;
const valueName = match[1].trim();
if (valueName.length > 0) valueNames.push(valueName);
}
return valueNames;
}
function normalizeVulkanLayerValueName(valueName: string): string {
return valueName.replace(/\//g, '\\').toLowerCase();
}
export function isFluxerGameCaptureVulkanLayerValue(valueName: string): boolean {
const segments = normalizeVulkanLayerValueName(valueName).split('\\');
const fileName = segments.at(-1) ?? '';
const packageDirectoryName = segments.at(-2) ?? '';
if (!FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME.test(fileName)) return false;
return FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES.has(packageDirectoryName);
}
function queryVulkanLayerRegistryValues(root: string): Array<string> {
try {
const stdout = execFileSync('reg.exe', ['query', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`], {
encoding: 'utf8',
windowsHide: true,
});
return parseRegistryValueNames(stdout);
} catch (error) {
const status = (error as {status?: number} | null)?.status;
if (status === 1) return [];
throw error;
}
}
function deleteVulkanLayerRegistryValue(root: string, valueName: string): void {
execFileSync('reg.exe', ['delete', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`, '/v', valueName, '/f'], {
stdio: 'ignore',
windowsHide: true,
});
}
function isSameVulkanLayerManifestPath(left: string, right: string): boolean {
return normalizeVulkanLayerValueName(left) === normalizeVulkanLayerValueName(right);
}
export function shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(
valueName: string,
keepManifestPath: string | null,
): boolean {
if (!isFluxerGameCaptureVulkanLayerValue(valueName)) return false;
return keepManifestPath === null || !isSameVulkanLayerManifestPath(valueName, keepManifestPath);
}
function removeStaleFluxerGameCaptureVulkanLayers(keepManifestPath: string | null): void {
if (process.platform !== 'win32') return;
for (const root of VULKAN_REGISTRY_ROOTS) {
let valueNames: Array<string>;
try {
valueNames = queryVulkanLayerRegistryValues(root);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to enumerate Vulkan implicit layer registry values', {root, error});
continue;
}
for (const valueName of valueNames) {
if (!shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(valueName, keepManifestPath)) continue;
try {
deleteVulkanLayerRegistryValue(root, valueName);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to remove stale Fluxer Vulkan layer registry value', {
root,
valueName,
error,
});
continue;
}
log.info('[VulkanGameCaptureLayer] Removed stale Fluxer Vulkan layer registry value', {root, valueName});
}
}
}
function loadWindowsGameCaptureModule(): WindowsGameCaptureModule | null {
if (process.platform !== 'win32') return null;
try {
const addon = requireModule('@fluxer/win-game-capture') as WindowsGameCaptureModule;
if (addon.loadError) {
log.warn('[VulkanGameCaptureLayer] Native game capture addon unavailable', addon.loadError);
return null;
}
return addon;
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to load the native game capture addon', error);
return null;
}
}
export function initializeWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
if (!addon || addon.isGameCaptureHookAvailable?.() !== true) {
removeStaleFluxerGameCaptureVulkanLayers(null);
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer left unregistered; hook-based game capture is disabled');
return;
}
try {
const manifestPath = addon.resolveVulkanLayerManifestPath?.() ?? null;
removeStaleFluxerGameCaptureVulkanLayers(manifestPath);
const registered = addon.registerVulkanLayerManifest?.() ?? false;
const state = addon.getVulkanLayerRegistrationState?.() ?? null;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer registration checked', {
registered,
manifestPath,
state,
});
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to register Vulkan implicit layer', error);
}
}
export function unregisterWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
try {
const unregistered = addon?.unregisterVulkanLayerManifest?.() ?? false;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer unregistration attempted', {
unregistered,
manifestPath: addon?.resolveVulkanLayerManifestPath?.() ?? null,
});
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to unregister Vulkan implicit layer', error);
}
removeStaleFluxerGameCaptureVulkanLayers(null);
}
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync} from 'node:child_process';
import log from 'electron-log';
const VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY = 'Software\\Khronos\\Vulkan\\ImplicitLayers';
const VULKAN_REGISTRY_ROOTS = ['HKCU', 'HKLM'] as const;
const FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME = /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/;
const FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES = new Set(['win-game-capture', 'win-screen-capture']);
function parseRegistryValueNames(stdout: string): Array<string> {
const valueNames: Array<string> = [];
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
const match = trimmed.match(/^(.*?)\s+REG_DWORD\s+(?:0x[0-9a-f]+|\d+)$/i);
if (!match) continue;
const valueName = match[1].trim();
if (valueName.length > 0) valueNames.push(valueName);
}
return valueNames;
}
function normalizeVulkanLayerValueName(valueName: string): string {
return valueName.replace(/\//g, '\\').toLowerCase();
}
function isFluxerVulkanLayerValue(valueName: string): boolean {
const segments = normalizeVulkanLayerValueName(valueName).split('\\');
const fileName = segments.at(-1) ?? '';
const packageDirectoryName = segments.at(-2) ?? '';
if (!FLUXER_VULKAN_LAYER_MANIFEST_FILE_NAME.test(fileName)) return false;
return FLUXER_VULKAN_LAYER_PACKAGE_DIRECTORY_NAMES.has(packageDirectoryName);
}
function queryVulkanLayerRegistryValues(root: string): Array<string> {
try {
const stdout = execFileSync('reg.exe', ['query', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`], {
encoding: 'utf8',
windowsHide: true,
});
return parseRegistryValueNames(stdout);
} catch (error) {
const status = (error as {status?: number} | null)?.status;
if (status === 1) return [];
throw error;
}
}
function deleteVulkanLayerRegistryValue(root: string, valueName: string): void {
execFileSync('reg.exe', ['delete', `${root}\\${VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY}`, '/v', valueName, '/f'], {
stdio: 'ignore',
windowsHide: true,
});
}
export function removeFluxerVulkanLayerRegistrations(): void {
if (process.platform !== 'win32') return;
for (const root of VULKAN_REGISTRY_ROOTS) {
let valueNames: Array<string>;
try {
valueNames = queryVulkanLayerRegistryValues(root);
} catch (error) {
log.warn('[VulkanLayerCleanup] Failed to enumerate Vulkan implicit layer registry values', {root, error});
continue;
}
for (const valueName of valueNames) {
if (!isFluxerVulkanLayerValue(valueName)) continue;
try {
deleteVulkanLayerRegistryValue(root, valueName);
} catch (error) {
log.warn('[VulkanLayerCleanup] Failed to remove Fluxer Vulkan layer registry value', {
root,
valueName,
error,
});
continue;
}
log.info('[VulkanLayerCleanup] Removed Fluxer Vulkan layer registry value', {root, valueName});
}
}
}
+3 -3
View File
@@ -78,7 +78,7 @@ import {
setQuitting,
showWindow,
} from '@electron/main/Window';
import {initializeWindowsVulkanGameCaptureLayer} from '@electron/main/WindowsVulkanGameCaptureLayer';
import {removeFluxerVulkanLayerRegistrations} from '@electron/main/WindowsVulkanLayerCleanup';
import {app, dialog, netLog} from 'electron';
import log from 'electron-log';
@@ -378,9 +378,9 @@ if (launchConfigurationError) {
log.error('[Init] Failed to register native audio handlers:', error);
}
try {
runStartupPhase('vulkan-game-capture-layer', initializeWindowsVulkanGameCaptureLayer);
runStartupPhase('vulkan-layer-cleanup', removeFluxerVulkanLayerRegistrations);
} catch (error: unknown) {
log.error('[Init] Failed to initialize Vulkan game capture layer:', error);
log.error('[Init] Failed to remove stale Vulkan layer registrations:', error);
}
try {
runStartupPhase('native-screen-capture-handlers', registerNativeScreenCaptureHandlers);