Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 104 additions & 6 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,21 @@ import {
import { enableRendererHeapHeadroom } from './startup/renderer-heap-headroom'
import { ensureVirtualDisplayForHeadlessServe } from './startup/ensure-virtual-display'
import {
markGpuFallbackUserNotified,
readActiveGpuFallbackMarker,
writeGpuFallbackMarker,
type GpuFallbackEnvironment,
type WindowsGpuFallbackEnvironment
} from './startup/gpu-fallback-marker'
import { applyGpuFallbackCommandLineSwitches } from './startup/gpu-fallback-switches'
import {
applyWindowsSoftwareGpuFallback,
buildSoftwareGpuFallbackNotice,
isSoftwareGpuEnvRequested,
shouldPresentSoftwareGpuFallbackNotice,
WINDOWS_SOFTWARE_GPU_SWITCHES,
type SoftwareGpuFallbackSource
} from './startup/windows-software-gpu'
import {
DEFAULT_GPU_CRASH_FALLBACK_THRESHOLD,
DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
Expand Down Expand Up @@ -376,6 +385,9 @@ const gpuCrashFallbackTracker = new GpuCrashFallbackTracker({
})
let gpuFallbackActiveThisLaunch = false
let gpuFeatureStatus: Electron.GPUFeatureStatus | null = null
let gpuFallbackSourceThisLaunch: SoftwareGpuFallbackSource | null = null
let gpuFallbackMarkerAlreadyNotified = false
let gpuFallbackNoticePresentedThisSession = false
let localPtyStartupReady: Promise<void> = Promise.resolve()
let localPtyProviderStartupReady: Promise<void> = Promise.resolve()
const AGENT_STATE_CRASH_BREADCRUMB_MIN_INTERVAL_MS = 30_000
Expand Down Expand Up @@ -1293,6 +1305,16 @@ function openMainWindow(): BrowserWindow {
window.once('ready-to-show', () => {
logStartupMilestone('ready-to-show')
setImmediate(createSystemTrayDeferred)
// Why: inform once after first paint so software-GPU recovery is not silent (#10097 review).
setImmediate(() => {
void maybePresentSoftwareGpuFallbackNotice(window)
})
})
// Why: ready-to-show can stall on broken GPUs; the reveal fallback still emits show.
window.once('show', () => {
setImmediate(() => {
void maybePresentSoftwareGpuFallbackNotice(window)
})
})
const trayCreateFallback = setTimeout(createSystemTrayDeferred, TRAY_CREATE_FALLBACK_MS)
trayCreateFallback.unref?.()
Expand Down Expand Up @@ -1570,26 +1592,102 @@ function getWindowsGpuFallbackEnvironment(): WindowsGpuFallbackEnvironment | nul
return { ...environment, platform: 'win32' }
}

// Why: read the GPU-fallback marker before app.whenReady() so app.disableHardwareAcceleration() takes effect. Windows desktop only.
// Why: software GPU flags must be set before app.whenReady(); marker is sticky for this build, ORCA_SOFTWARE_GPU opts in without a crash burst.
function maybeApplyGpuFallbackForThisLaunch(): void {
if (isServeMode || process.platform !== 'win32') {
return
}
const marker = readActiveGpuFallbackMarker(app.getPath('userData'), getGpuFallbackEnvironment())
if (!marker) {
const envRequested = isSoftwareGpuEnvRequested()
const marker = envRequested
? null
: readActiveGpuFallbackMarker(app.getPath('userData'), getGpuFallbackEnvironment())
if (!envRequested && !marker) {
return
}
app.disableHardwareAcceleration()
const appliedSwitches = applyGpuFallbackCommandLineSwitches(app.commandLine, process.platform)
let appliedSwitches: readonly string[] = []
if (envRequested) {
// Why: #10093 — operator first-launch opt-in for virtual displays (ORCA_SOFTWARE_GPU).
// Post-crash marker path keeps main's non-SwiftShader switches (security: in-process + no untrusted WebGL parse).
applyWindowsSoftwareGpuFallback(app)
appliedSwitches = WINDOWS_SOFTWARE_GPU_SWITCHES.map((flag) =>
'value' in flag && flag.value !== undefined ? `${flag.name}=${flag.value}` : flag.name
)
} else {
app.disableHardwareAcceleration()
appliedSwitches = applyGpuFallbackCommandLineSwitches(app.commandLine, process.platform)
}
gpuFallbackActiveThisLaunch = true
gpuFallbackSourceThisLaunch = envRequested ? 'env' : 'marker'
gpuFallbackMarkerAlreadyNotified =
typeof marker?.userNotifiedAt === 'number' && Number.isFinite(marker.userNotifiedAt)
// 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,
crashesInWindow: marker?.crashesInWindow ?? 0,
source: gpuFallbackSourceThisLaunch,
switches: appliedSwitches.join(',')
})
}

// Why: software GPU is a silent recovery path unless we tell the user once (marker sticky / env per session).
async function maybePresentSoftwareGpuFallbackNotice(
targetWindow?: BrowserWindow | null
): Promise<void> {
if (
!shouldPresentSoftwareGpuFallbackNotice({
active: gpuFallbackActiveThisLaunch,
source: gpuFallbackSourceThisLaunch,
alreadyPresentedThisSession: gpuFallbackNoticePresentedThisSession,
markerAlreadyNotified: gpuFallbackMarkerAlreadyNotified
}) ||
isQuitting
) {
return
}
const source = gpuFallbackSourceThisLaunch
if (!source) {
return
}
gpuFallbackNoticePresentedThisSession = true
const notice = buildSoftwareGpuFallbackNotice(source)
const options = {
type: 'info' as const,
buttons: ['OK'],
defaultId: 0,
title: notice.title,
message: notice.message,
detail: notice.detail
}
try {
const window =
targetWindow && !targetWindow.isDestroyed()
? targetWindow
: mainWindow && !mainWindow.isDestroyed()
? mainWindow
: undefined
if (window) {
await dialog.showMessageBox(window, options)
} else {
await dialog.showMessageBox(options)
}
} catch (error) {
console.warn('[gpu-fallback] failed to present software GPU notice:', error)
// Why: allow a later show/ready-to-show retry if the first dialog attempt failed.
gpuFallbackNoticePresentedThisSession = false
return
}
if (source === 'marker') {
try {
if (markGpuFallbackUserNotified(app.getPath('userData'))) {
gpuFallbackMarkerAlreadyNotified = true
}
} catch (error) {
console.warn('[gpu-fallback] failed to persist notice acknowledgment:', error)
}
}
recordCrashBreadcrumb('gpu_fallback_notice_presented', { source })
}

// Why: a burst of GPU child crashes means HW acceleration is unusable — persist a build-scoped marker and offer software rendering.
async function handleGpuChildCrash(reason: string, exitCode: number | null): Promise<void> {
// Software rendering already active or shutting down: nothing more to do.
Expand Down
20 changes: 20 additions & 0 deletions src/main/startup/gpu-fallback-marker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
GPU_FALLBACK_MARKER_FILE,
clearGpuFallbackMarker,
markGpuFallbackUserNotified,
readActiveGpuFallbackMarker,
readGpuFallbackMarker,
writeGpuFallbackMarker
Expand Down Expand Up @@ -114,4 +115,23 @@ describe('gpu-fallback-marker', () => {
clearGpuFallbackMarker(userDataPath)
expect(readGpuFallbackMarker(userDataPath)).toBeNull()
})

it('round-trips optional userNotifiedAt and is idempotent', () => {
writeGpuFallbackMarker(userDataPath, { engagedAt: 10, crashesInWindow: 3 }, environment)
expect(readGpuFallbackMarker(userDataPath)?.userNotifiedAt).toBeUndefined()

expect(markGpuFallbackUserNotified(userDataPath, 999)).toBe(true)
expect(readGpuFallbackMarker(userDataPath)).toMatchObject({
engagedAt: 10,
crashesInWindow: 3,
userNotifiedAt: 999
})

expect(markGpuFallbackUserNotified(userDataPath, 1_000_000)).toBe(true)
expect(readGpuFallbackMarker(userDataPath)?.userNotifiedAt).toBe(999)
})

it('returns false when marking notify without a marker', () => {
expect(markGpuFallbackUserNotified(userDataPath)).toBe(false)
})
})
44 changes: 41 additions & 3 deletions src/main/startup/gpu-fallback-marker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ export type GpuFallbackMarker = {
appVersion: string
electronVersion: string
platform: 'win32'
/** When set, the user was already told software GPU is active for this marker. */
userNotifiedAt?: number
}

function markerPath(userDataPath: string): string {
return join(userDataPath, GPU_FALLBACK_MARKER_FILE)
}

function readOptionalFiniteNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}

export function readGpuFallbackMarker(userDataPath: string): GpuFallbackMarker | null {
try {
const parsed = JSON.parse(readFileSync(markerPath(userDataPath), 'utf-8')) as Partial<
Expand All @@ -53,13 +59,15 @@ export function readGpuFallbackMarker(userDataPath: string): GpuFallbackMarker |
) {
return null
}
const userNotifiedAt = readOptionalFiniteNumber(parsed.userNotifiedAt)
return {
schemeVersion: GPU_FALLBACK_SCHEME_VERSION,
engagedAt: parsed.engagedAt,
crashesInWindow: parsed.crashesInWindow,
appVersion: parsed.appVersion,
electronVersion: parsed.electronVersion,
platform: parsed.platform
platform: parsed.platform,
...(userNotifiedAt !== undefined ? { userNotifiedAt } : {})
}
} catch {
// missing or corrupt means no fallback requested
Expand All @@ -69,20 +77,50 @@ export function readGpuFallbackMarker(userDataPath: string): GpuFallbackMarker |

export function writeGpuFallbackMarker(
userDataPath: string,
info: { engagedAt: number; crashesInWindow: number },
info: { engagedAt: number; crashesInWindow: number; userNotifiedAt?: number },
environment: WindowsGpuFallbackEnvironment
): void {
const userNotifiedAt = readOptionalFiniteNumber(info.userNotifiedAt)
const marker: GpuFallbackMarker = {
schemeVersion: GPU_FALLBACK_SCHEME_VERSION,
engagedAt: info.engagedAt,
crashesInWindow: info.crashesInWindow,
appVersion: environment.appVersion,
electronVersion: environment.electronVersion,
platform: 'win32'
platform: 'win32',
...(userNotifiedAt !== undefined ? { userNotifiedAt } : {})
}
writeFileSync(markerPath(userDataPath), JSON.stringify(marker))
}

/** Persist that the software-GPU notice was shown for the active sticky marker. */
export function markGpuFallbackUserNotified(
userDataPath: string,
notifiedAt: number = Date.now()
): boolean {
const marker = readGpuFallbackMarker(userDataPath)
if (!marker) {
return false
}
if (readOptionalFiniteNumber(marker.userNotifiedAt) !== undefined) {
return true
}
writeGpuFallbackMarker(
userDataPath,
{
engagedAt: marker.engagedAt,
crashesInWindow: marker.crashesInWindow,
userNotifiedAt: notifiedAt
},
{
appVersion: marker.appVersion,
electronVersion: marker.electronVersion,
platform: 'win32'
}
)
return true
}

export function clearGpuFallbackMarker(userDataPath: string): void {
try {
rmSync(markerPath(userDataPath), { force: true })
Expand Down
Loading