Preflight Guardrails
Status: design — implementation tracked in Linear EC-399 (parent) and EC-400 / EC-401 / EC-402 / EC-403 (children).
Preflight guardrails make setup self-correcting so a contributor can't silently record a session that won't be usable. Problems are surfaced ahead of time — at game load — rather than discovered after a wasted session. Two behaviours:
- Recording outside a group → warn but allow. A clear, dismissible warning; never a hard block.
- On game load → a preflight surface that flags wrong resolution, poor FPS, mic not picking up anything, and group membership, so each can be fixed before play.
Design principle: reuse the environment-check path
The launcher already has the exact mechanism this needs. Each guardrail surfaced in the launcher is expressed as a warning-severity CoreUiErrorDirective and flows through the existing environment-check pipeline — no new transport, no new UI shell. Hard blockers stay reserved for true hard-stops (auth, non-16:9 window at record time, disabled mic per EC-306).
Exception (EC-400): guardrails that the player must see while in-game can't use the launcher banner (it's occluded) — they surface through the core's in-game overlay instead: a persistent
PreflightOverlaypanel (a sibling of the stats / mic / gpu HUDs, toggled with F8), shown only while a check is failing. That path is core-rendered, so it needs a small transport of its own: the localized row labels are pushed to the core viaPOST /api/overlay/preflight-labels. See §1 and Boundary consistency.
Existing building blocks
| Piece | Location | Role |
|---|---|---|
| Environment-check IPC | playroll-ui GET_ENVIRONMENT_CHECK (src/main/ipc/handlers.ts) | Returns { blockers, warnings } of CoreUiErrorDirective |
useEnvironmentCheck | playroll-ui src/renderer/hooks/useEnvironmentCheck.ts | Splits into hasBlockers + warning list |
EnvironmentCheckModal | playroll-ui src/renderer/features/launcher/components/ | Hard-stop modal for blockers |
EnvironmentWarningBanner | same dir | Stacked dismissible warning cards — the warn-but-allow surface |
system-check | playroll-ui src/main/system-check/ | Probes os/cpu/ram/disk/gpu → ok | warning | blocking |
resolution-probe | playroll-ui src/main/resolution-probe/ | Reads game config → effective resolution (file-read only, anti-cheat-safe) |
fps_profiler | playroll-cpp include/service/fps_profiler.hpp | In-capture FPS warning events |
| Mic level | playroll-cpp include/service/mic_overlay.hpp (MicData{muted, vuLevel}) | Live VU level via MicProvider |
| Lobby (group) state | playroll-cpp lobby_state.hpp + playroll-ui src/main/lobby/lobby-manager.ts | Active lobby in both processes; lobby.active in renderer |
The four guardrails
1. Group membership (EC-400) — cpp + ui
lobby.active is known in both processes. A LobbyGameMismatch blocker
already exists (recording the wrong game for the lobby), but nothing signals
"you're recording solo".
Surface — a persistent in-game preflight panel, not the launcher banner.
The guardrail must be visible while the player is in the game (both before and
during recording) — which is exactly when the launcher is occluded by the
fullscreen game, so a launcher-chrome banner would render where nobody is
looking. It is therefore delivered as a row in a new in-game preflight
overlay (PreflightOverlay), a persistent click-through Direct2D panel that is
a sibling of the stats / mic / gpu HUDs: anchored top-left of the game window,
toggled with the same F8 overlay cluster, and shown only while at least one
check is failing (it hides itself when everything is in order). Each guardrail
is one row with a severity marker (! warning / ✕ error) + a short label.
EC-400 ships the group row; EC-401 / EC-402 / EC-403 add resolution / mic /
fps rows to the same panel. This is the shared preflight surface — not a
separate per-guardrail notification.
Overlay convergence (now the default). Since the cross-platform overlay shipped, the native Direct2D
PreflightOverlayis suppressed by default — the Electron overlay's Alerts widget surfaces these guardrails instead, derived renderer-side from the recorder status (windowAspectOk, mic muted/silent,perf.severity, lobby/solo). The native panel and thePOST /api/overlay/preflight-labelslabel transport described below remain the legacy path, reachable withPLAYROLL_LEGACY_OVERLAY=1(Windows, for diagnosis). The guardrail logic is unchanged; only the surface that paints it moved from the core's D2D panel to the Electron overlay.
Why a panel, not a record-start toast. An earlier iteration fired a one-shot notification at record-start; it was rejected because record-start is already too late to fix anything, and a transient toast can't be re-checked. A persistent panel shown from game-load lets the player see and fix the issue before they record, and clears itself the moment they do (e.g. they join a lobby).
Mechanism — core-driven, UI-localized. The core owns the overlay and already
holds the lobby context, so its preflight-overlay provider checks lobby.active
on each tick and emits the group row when solo. The row's copy is owned by the
UI (i18n): the renderer pushes the localized checklist labels (checkId → text)
to the core via POST /api/overlay/preflight-labels, re-pushing on connect /
language-change; the core caches them and looks each row up by id. A missing
label (older UI) simply omits that row — never English-fallback.
Consequence (resolved). Recording solo has no downside: a solo session validates and pays out exactly like a grouped one. Grouping only adds an optional multi-POV view. The copy therefore frames grouping as a bonus, never solo as a penalty.
2. Wrong resolution (EC-401) — UI only
windowAspectBlocked blocks recording at start time for non-16:9, but only when the user hits record. resolution-probe already knows the game's effective resolution at detection time. A new resolution entry in the system-check report emits an early, explanatory warning (non-16:9, or below a resolution floor) so it's fixed before play. The record-time 16:9 hard block remains the backstop.
3. Mic not picking up (EC-402) — cpp + ui
MicData.vuLevel gives a live level; VoicePrerequisiteStartGate (EC-306) already blocks a disabled mic. Nothing detects an enabled-but-silent mic (wrong device, hardware mute, zero gain). A short cpp silence detector samples vuLevel over a rolling window after game load and sets a micSilent status flag; the UI maps it to a dismissible warning. Warn-but-allow.
4. Poor FPS (EC-403) — cpp + ui
True FPS cannot be measured before capture starts, so this is delivered in two parts: a predictive game-load warning (resolution + GPU + target FPS heuristic, worded as "may"), and an early in-capture warning that surfaces the existing fps_profiler WarningRaised event (≈10s warmup) into the banner while the session is still salvageable.
Boundary consistency
Real-time "FPS dropping / audio device changed" alerts belong to the in-game status island, not the Notifications tab (per the notification delivery flow boundary contract).
Surface follows visibility, not category. The original framing put all four
guardrails in the launcher's EnvironmentWarningBanner ("preflight = launcher
chrome"). EC-400 corrected that: a guardrail that fires at or after game load
must surface where the player actually is — the in-game overlay — because
the launcher is occluded by the (usually fullscreen) game by then. The launcher
banner remains the right home only for guardrails surfaced before launch,
while the player is still looking at the launcher. The same occlusion logic
likely applies to EC-401 / EC-402 / EC-403 (which also fire at or after game
load); revisit their surface when they are built rather than defaulting them to
the banner.
Open questions
- Resolution: block only on non-16:9 (current) or also warn below a floor (e.g. 720p)? What is the supported set?
- FPS: target FPS and the "poor" cutoff?
fps_profilerdefaults to 95% of target — reuse it? - Mic: silence window length and the level threshold that counts as "silent"?
Group: the real consequence of recording solo, needed for honest copy.Resolved (EC-400): none — a solo recording validates and pays out the same; grouping only adds an optional multi-POV view. Copy frames grouping as a bonus, never solo as a penalty.