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