Files
orca/src/main/startup/gpu-lifecycle.ts
T
Neil e5ca694805 fix(windows): repair the poisoned install-dir ACL before the window, not after
The install-dir LPAC ACL poison (electron/electron#51761) still costs every
affected machine at least one crash: the probe that detects it is
setImmediate-deferred and answers 0.9-3.0s in, while createMainWindow runs
synchronously in the same frame and its renderer dies at init 48-1373ms later.

- Persist the poison verdict the moment the probe reports it, and await the
  repair (bounded at 20s) before any window is created on a launch that already
  carries the marker.
- Do not engage the GPU safe-graphics fallback while the install-dir ACL verdict
  is poisoned or still outstanding. Safe graphics does not rescue a poisoned
  tree, and --in-process-gpu removes the GPU child, erasing the sibling-death
  evidence that identifies the shape (4 field reports landed in 'misc' this way).
- Clear the safe-graphics marker once the repair lands, so a repaired machine
  stops launching software-rendered for the rest of that build.
- Give the repair marker a bounded retry budget: it was written on failure and
  matched regardless of outcome, so one transient failure pinned a machine to
  'marker-hit' for the life of that version.
2026-09-02 21:25:11 -07:00

191 lines
7.2 KiB
TypeScript

import { app, type BrowserWindow } from 'electron'
import { relaunchApp } from '../app-relaunch'
import { destroySystemTray } from '../tray/system-tray'
import { applyGpuFallbackCommandLineSwitches } from './gpu-fallback-switches'
import {
clearGpuFallbackMarker,
readActiveGpuFallbackMarker,
writeGpuFallbackMarker,
type WindowsGpuFallbackEnvironment
} from './gpu-fallback-marker'
import {
handleGpuFallbackRecoveredLaunch,
promptForGpuFallbackRecoveredLaunch
} from '../crash-reporting/gpu-fallback-recovered-launch'
import { promptForGpuFallbackRestart } from '../crash-reporting/gpu-fallback-restart-prompt'
import { engageGpuFallbackAfterCrashBurst } from '../crash-reporting/gpu-fallback-engagement'
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
import { recordDurableCrashBreadcrumb } from '../crash-reporting/durable-crash-breadcrumb'
import { isInstallDirAclSuspect } from './windows-install-dir-acl-recovery'
import { mainProcessState as state, gpuFallbackEnvironment } from './main-process-state'
import { createGpuAccelerationAboutPanelOptions } from '../menu/gpu-acceleration-about-panel'
export function updateGpuAccelerationAboutPanel(): void {
app.setAboutPanelOptions(
createGpuAccelerationAboutPanelOptions({
appName: app.name,
appVersion: app.getVersion(),
platform: process.platform,
gpuFallbackActive: state.gpuFallbackActiveThisLaunch,
gpuFeatureStatus: state.gpuFeatureStatus
})
)
}
function getWindowsGpuFallbackEnvironment(): WindowsGpuFallbackEnvironment | null {
const environment = gpuFallbackEnvironment()
return environment.platform === 'win32' ? { ...environment, platform: 'win32' } : null
}
// Writes both crash-time and post-recovery consent states through one build-scoped path.
function persistGpuFallbackMarker(
userDataPath: string,
info: { engagedAt: number; crashesInWindow: number; userConfirmed: boolean }
): boolean {
const environment = getWindowsGpuFallbackEnvironment()
if (!environment) {
return false
}
try {
writeGpuFallbackMarker(userDataPath, info, environment)
return true
} catch (error) {
console.warn('[gpu-fallback] failed to persist marker:', error)
return false
}
}
// Read before app.whenReady() so app.disableHardwareAcceleration() takes effect. Windows desktop only.
export function maybeApplyGpuFallbackForThisLaunch(): void {
if (state.isServeMode || process.platform !== 'win32') {
return
}
const marker = readActiveGpuFallbackMarker(app.getPath('userData'), gpuFallbackEnvironment())
if (!marker) {
return
}
state.activeGpuFallbackMarker = marker
app.disableHardwareAcceleration()
const appliedSwitches = applyGpuFallbackCommandLineSwitches(app.commandLine, process.platform)
state.gpuFallbackActiveThisLaunch = true
// Why: with no GPU child left, child-process-gone can't report a GPU fault, so
// name the applied switches in the trail any later crash report carries.
recordCrashBreadcrumb('gpu_fallback_applied', {
crashesInWindow: marker.crashesInWindow,
switches: appliedSwitches.join(',')
})
}
export async function presentGpuFallbackRecoveredLaunchPrompt(
window: BrowserWindow
): Promise<void> {
const marker = state.activeGpuFallbackMarker
if (!marker || marker.userConfirmed || window.isDestroyed() || state.isQuitting) {
return
}
// One prompt per process. A failure leaves the on-disk marker unconfirmed so the next launch retries.
state.activeGpuFallbackMarker = null
const userDataPath = app.getPath('userData')
await handleGpuFallbackRecoveredLaunch({
isQuitting: () => state.isQuitting,
prompt: () => promptForGpuFallbackRecoveredLaunch(window),
confirmSafeGraphics: () => {
persistGpuFallbackMarker(userDataPath, {
engagedAt: marker.engagedAt,
crashesInWindow: marker.crashesInWindow,
userConfirmed: true
})
},
clearSafeGraphics: () => clearGpuFallbackMarker(userDataPath),
onPromptFailed: (error) =>
console.warn('[gpu-fallback] failed to show recovered-launch prompt:', error),
onSafeGraphicsKept: () =>
recordDurableCrashBreadcrumb('gpu_fallback_safe_graphics_kept', {
crashesInWindow: marker.crashesInWindow
}),
restartWithHardware: () => {
state.isQuitting = true
relaunchApp('gpu-fallback', {
mode: 'hardware-retry',
crashesInWindow: marker.crashesInWindow
})
destroySystemTray()
app.exit(0)
}
})
}
// Why: a burst of GPU child crashes means HW acceleration is unusable — persist a build-scoped marker and offer software rendering.
export async function handleGpuChildCrash(
reason: string,
exitCode: number | null,
crashedAt: number
): Promise<void> {
// Software rendering already active or shutting down: nothing more to do.
if (state.gpuFallbackActiveThisLaunch || state.isQuitting || state.isServeMode) {
return
}
// Why: a poisoned install DACL kills the GPU child exactly like a bad driver, but
// safe graphics does not rescue it and --in-process-gpu removes the GPU child, so
// every later crash loses the sibling deaths that identify the real cause.
if (isInstallDirAclSuspect()) {
return
}
const result = state.gpuCrashFallbackTracker.recordGpuCrash(crashedAt)
if (!result.shouldEngageFallback) {
return
}
const fallbackData = { processReason: reason, exitCode, crashesInWindow: result.crashesInWindow }
const userDataPath = app.getPath('userData')
await engageGpuFallbackAfterCrashBurst(
{ reason, exitCode, crashesInWindow: result.crashesInWindow, engagedAt: Date.now() },
{
isQuitting: () => state.isQuitting,
onEngaged: (engagement) =>
recordCrashBreadcrumb('gpu_fallback_engaged', {
reason: engagement.reason,
exitCode: engagement.exitCode,
crashesInWindow: engagement.crashesInWindow
}),
persistMarker: (engagement) =>
persistGpuFallbackMarker(userDataPath, {
engagedAt: engagement.engagedAt,
crashesInWindow: engagement.crashesInWindow,
userConfirmed: false
}),
confirmMarker: (engagement) => {
persistGpuFallbackMarker(userDataPath, {
engagedAt: engagement.engagedAt,
crashesInWindow: engagement.crashesInWindow,
userConfirmed: true
})
},
clearMarker: () => clearGpuFallbackMarker(userDataPath),
promptForRestart: () =>
promptForGpuFallbackRestart(
state.mainWindow && !state.mainWindow.isDestroyed() ? state.mainWindow : undefined
),
onPromptFailed: (error) =>
console.warn('[gpu-fallback] failed to show restart prompt:', error),
onRestartDeferred: () =>
recordDurableCrashBreadcrumb('gpu_fallback_restart_deferred', fallbackData),
restartIntoSafeGraphics: () => {
state.isQuitting = true
relaunchApp('gpu-fallback', fallbackData)
destroySystemTray()
app.exit(0)
}
}
)
}
export function registerGpuLifecycleHandlers(): void {
app.on('gpu-info-update', () => {
state.gpuFeatureStatus = app.getGPUFeatureStatus()
state.gpuCrashDiagnostics?.warm()
if (app.isReady()) {
updateGpuAccelerationAboutPanel()
}
})
}