# ExoSnap Product Specification **Status:** Living document, pre-1.0. This is the durable, tracked record of ExoSnap's user-visible product behavior and the reasoning behind it. It describes *what the product does and why* — not implementation internals. Where a behavior is genuinely undecided, it is marked **Open** rather than invented. Pre-1.0 note: settings, preset, and recording-history schemas are not frozen and may change incompatibly before 1.0.0. When product decisions here disagree with older internal notes, this document and `CLAUDE.md` are authoritative. --- ## 1. Product intent and principles ExoSnap is a Windows-native screen, application, and region recorder with a native NVENC pipeline. It records MKV first and delivers MP4 by remuxing on stop. It is built around a small set of principles that shape every visible decision: - **Diagnostics-first.** Readiness is a feature, not an afterthought. The product tells the user up front whether a recording will succeed, classifies problems while recording, and reports health afterward. - **Reliability-first.** Recording, recovery, and honest status come before breadth. An existing NVIDIA user benefits from reliable capture and recovery before the audience is widened to other vendors. - **Honesty over reassurance.** The UI never offers a combination it has not vetted, never labels an encoder mode as something it is not (for example, never calling NVENC "CRF"), and states file-size and capability limits plainly ("approximately N GB", not a byte-exact promise). - **Calm, not alarmist.** Diagnostics defaults to a quiet, factual tone. Only real, measured problems are surfaced; each problem carries one primary fix; deeper detail sits behind collapsed reference rows; hard blockers are always visible. - **Privacy by default.** No analytics, no telemetry, no account. The app makes no network connection unless the user opts in to a specific feature. - **Engine stays UI-agnostic.** Track resolution, capability, and reconciliation live in the engine; the UI submits editable models and renders resolved results. --- ## 2. Navigation and information architecture Every destination is directly reachable in the title band, in this order: **Record · Settings · Diagnostics · Logs · About** There is no overflow menu and no secondary navigation tier. Five words fit the band at the **860 × 700** minimum window, so hiding three of them behind a glyph bought nothing and cost a click plus a menu on the way to Diagnostics — the page a user opens precisely when something is already wrong. Below the regular width class the tabs give up horizontal padding (12 px → 8 px) and the gap after the wordmark narrows; the labels are never shortened, elided, or set smaller, and the window buttons never give up room (§ title bar below). The dividing rule for content is: - **Settings** owns what the user tells ExoSnap to do. - **Diagnostics** owns what ExoSnap observes about the machine and the runtime. A read-only hardware inspector therefore does not live in Settings, and an editable product setting does not live in Diagnostics. - **Record** — the operational view: capture target, readiness, live preview before recording, and the live runtime (technical) view while recording. - **Settings** - unified recording configuration in two balanced card columns. The left column is **Recording format · Audio sources · Output · App behaviour · Hotkeys · Appearance**; the right is **Video quality & timing · Audio encoding · Webcam · Overlays · Updates · Support & diagnostics**. There is no separate Advanced section - a global **Expert** toggle, a Settings-only control, reveals additional rows in place within a section rather than hiding or revealing a whole card (§12). Hotkeys is an embedded card, not a separate nav item. - **Diagnostics** — a state-ordered page (§11): the live, changeable environment as readiness tiles (disk, display, audio, encoder) and live tiles while recording, plus a **Hardware capabilities** reference row: one card per detected DXGI adapter (iGPU/dGPU), with the per-adapter capability matrix for whichever card is being inspected — codec support and its provenance, per-codec 8-bit 4:4:4 (YUV444) encode support probed on that specific GPU (H.264 / HEVC — AV1 is 4:2:0 only), and per-codec NVENC advanced-encode facts: B-frames (max count), Lookahead and Temporal AQ. All of it is informational; no toggle reads these facts. Selecting a card is **inspection only** — the encode device is not a user choice (see §2.1). Hardware capabilities is one of the page's collapsed reference rows, each with a one-line summary in its header; expanding this one is what starts the adapter scan, so a cold DXGI enumeration and NVENC probe are never paid for unasked. - **Logs** — runtime events and per-session recording diagnostics, a **Startup** latency table, and a **Create support bundle** action. - **About** — application identity, build metadata, and links. The metadata table permanently shows **Version** (the full release version, e.g. `0.9.0-rc4`; developer builds honestly report `0.9.0-dev`), **Commit** (shortened, linking to the full SHA on GitHub), **Built** (the deterministic source-date timestamp, `YYYY-MM-DD HH:MM UTC`), **Install** (Portable / MSI / Scoop) and **Channel** (Stable / Preview). Deviations from an official release build appear only when true, as conditional notices: *Unofficial build*, *Debug build*, *Dirty source tree*. **Copy details** copies a full support block (version, tag, full commit SHA, build time, CI build ID, architecture, configuration, official flag, install mode, channel, executable path and its SHA-256 — the hash is computed lazily off the UI thread on first click). **Edit / Output / Save** is a post-stop **workspace over the Record page**, not a nav item and not a persisted project editor. After recording stops, the surface opens over Record as **one view**: player, trim timeline, a right rail with the details and export cards, and the post-flight report at the right end of its header. It spans the client area below the real title bar, so the window keeps its title band and can be moved, minimized and closed throughout. **Edit state is deliberately temporary.** Trim points and future cut operations are an in-memory export recipe, not a saved project. **Back**, Escape, opening another clip, or navigating to Settings, Diagnostics, Logs or About closes the workspace and discards that recipe immediately, without a dirty badge, confirmation, draft file or recovery offer. Returning to Record shows the normal Completed state; opening Edit again starts clean from the source recording. This keeps the post-stop editor suitable for quick trimming and cutting without introducing project identity, draft cleanup, conflict resolution or replacement prompts. The five top-level destinations and `Ctrl+1`...`Ctrl+5` stay available, and navigation is unconditional. A **running export is the exception to the workspace lifetime**: pressing Export has already committed a snapshot of the current recipe, so that operation keeps running independently after navigation and reports its result through the notification system. Navigation does not cancel or roll it back. Blocking surfaces (recovery, crash report, recording error) still block navigation because they are modal about a question the user has not answered; Edit is not. The window controls remain unaffected. **The destination the user is already on is never disabled** — it is not a place they could be sent, it is where they are, and greying it out leaves the band showing no current destination at all. The other four are greyed while the surface is up. Every navigation intent answers to this one contract — a tab, a keyboard shortcut, a notification action, a jump from the Diagnostics page. There is no second path with different rules. ### 2.1 The encode device is not a user choice ExoSnap does not offer an encoder-device or encoder-backend selector, and the UI never implies one. The reason is the pipeline, not the design. The capture path creates one D3D11 device — matched to the adapter that owns the target monitor for DXGI Output Duplication, or the default adapter for window capture — and NVENC opens **on that same device**. There is no second, independently chosen encode device to point somewhere else, and no adapter preference in the recorder configuration. Presenting a picker would therefore either do nothing or require a cross-adapter transfer path the pipeline does not have. Consequences the UI must respect: - Diagnostics states which adapter is *actually* carrying the encode, and every other detected adapter reads as **not encoding** — a fact about now, never a promise about a backend. - Settings names that adapter too, as a read-only **"Encoding on"** row in *Video quality & timing* with the hint *"Fixed by the capture device — not a setting"*. It is shaped so it cannot be mistaken for a control: no select, no chevron, no affordance. "Which GPU is this running on" is the first thing a user checks when quality or performance surprises them, and sending them to a diagnostics panel for a settled fact made the fact feel provisional. The name comes from the startup capability probe, never from the Diagnostics adapter scan — a settings row must not start a DXGI enumeration. - Unshipped encoder backends (AMD/AMF, Intel Quick Sync, software x264/SVT-AV1) do **not** appear in production UI at all. Roadmap belongs in the repository roadmap, not in a product surface where it reads as an available capability. - If explicit encode-device selection is ever shipped, it needs a persistent hardware identity first: the DXGI adapter LUID identifies an adapter only within the current desktop session and must not be persisted across reboots. All shipped interface copy is English in the current release and is wrapped for Qt translation as it is introduced. English is the fallback locale. User-visible recovery and error copy follows the same rule; implementation-only identifiers and log fields are not localized. ### 2.2 Appearance and accent The product has exactly **two base appearances** — **Dark** (the default) and **Light** — and a small curated **accent** palette chosen independently of them. The default is **Dark + Aqua**, the Studio-Mint identity colour. The two axes are separate because coupling them served nobody: the four complete themes this replaces (`dark-default`, `dark-indigo`, `light-paper`, `light-slate`) each pinned one hue to one set of neutrals, so choosing indigo also meant accepting a different background, and two light themes existed mainly because neither could carry the other's accent. | Accent | Intent | |--------|--------| | **Aqua** (default) | Studio mint — the ExoSnap identity | | Sky | Petrol blue — cooler and quieter | | Violet | Periwinkle — more contrast against the neutrals | | Magenta | Warm pink — the most assertive of the four | The list is deliberately small, and deliberately all cool. **Coral, amber and green are the product's semantic colours**: recording/error, attention/held, and ready/success. Paused has its own semantic `paused` role whose shipped value is amber; warnings use the separate `warning` role even though it is also amber. A pause glyph or the word `Paused`, and a warning glyph or warning copy, always supplies the second cue. An accent sharing one of these hues would make an ordinary selection read as a state. **Semantic meaning wins over palette breadth**: no state colour is ever derived from the accent, and switching accent never changes what recording, paused, warning or ready look like. There is no free colour picker, no user-defined surface colour, and no separate primary/background/surface customisation. The accent drives selection: the active nav underline, the primary action, active toggles, focus rings and selected-card tint. Content drawn *on* a filled accent surface uses a curated contrasting ink per accent and appearance, never a derived one. **Blocked is a state, not the absence of one.** A control the product has taken away - a setting locked for the length of a recording, a webcam row with no camera attached - reads as blocked at a glance. Its label and current value remain readable; the interaction chrome, hint and secondary ink step down, and the control loses its raised fill and hover response. **The accent goes**, because the accent means live and selectable. What stays is the *shape* of the state: a blocked switch that is on keeps a filled track with its knob at the right end, filled in the dim ink rung rather than in the accent, so a locked page still answers "which sources am I recording?" without any control on it looking pressable. A row is dimmed only when **every** control in it is blocked; one live control beside an inert one keeps the row at full strength. **Light** is one base, rebuilt rather than inherited. It distinguishes four surface rungs — application background, primary surface, raised control surface, and hover/selected — none of which is pure white, plus restrained borders. Both light themes it replaces set the raised-control surface *and* the hover surface to pure white, which collapsed two rungs into one; that is why the light UI read as flat, and why a hovered control looked identical to a resting one. Hover steps back *down* towards the page — there is no headroom above white to step up into. The **ground is grey and the content on it is near-white**. The page carries most of the window's area, so a page only a step or two below its cards makes the whole surface glare and leaves the cards nothing to sit against; the ground drops rather than the cards dimming, which keeps the content bright and takes the glare out of the frame around it. Separation comes from the value steps alone — **no elevation shadows**. This is a tool surface, and a drop shadow under every card would be decoration standing in for a hierarchy the steps already carry. An installation that stored one of the four old theme ids is migrated to the closest pair on first launch and keeps the colour it had: | Stored theme | Appearance | Accent | |--------------|-----------|--------| | `dark-default` | Dark | Aqua | | `dark-indigo` | Dark | Violet | | `light-paper` | Light | **Sky** | | `light-slate` | Light | Violet | Mapped by the accent hue the user actually saw, not by position in the old list — which is why `light-paper` lands on Sky rather than on the default Aqua: its accent was petrol blue, exactly Sky's light value. An unreadable or absent preference resolves to **Dark + Aqua**; it never produces a blank or unstyled window. Every shipped appearance/accent pair is held to contrast thresholds by semantic role, not to one blanket ratio: **4.5:1** for text and for the ink on a filled accent or error control (WCAG 1.4.3 — the product's largest button label is 16 px DemiBold, which is not "large text", so nothing gets a discount), and **3:1** for the indicators that identify a control's state — the selected nav underline, an active control's ring, the keyboard focus ring, and the recording / caution / ready colours (WCAG 1.4.11). An **unavailable** control is exempt from both criteria, but the product promises it stays visible rather than disappearing (§8), so it is held to the same 3:1 anyway. A resting hairline is not: it separates surfaces, it is not what identifies a control. Settings → **Appearance** exposes the two axes as two rows: Appearance as a segmented Dark/Light control (two mutually exclusive values do not deserve a list that must be opened to see the other one), and Accent as a row of round swatches. Each swatch is drawn in the value the accent will actually take in the **current** appearance, so a light-mode swatch never shows its dark-mode colour. Selection is marked by a ring in the appearance's own text colour rather than in the accent — a swatch outlined in its own colour is invisible as a selection — and the ring stands **clear of the dot**, so the mark is a shape rather than a slightly heavier outline. Resting swatches carry no ring at all: the row has to answer "which one is active" at a glance, without the user clicking one to find out. Every swatch is keyboard reachable and carries the accent's name as its accessible name. --- The frameless main window asks Windows 11 for **rounded corners**, like every other top-level window, rather than relying on a default it would not otherwise get as a fully custom-chrome window. It also carries a **subtle native 1px border** in the active appearance's line color (it follows those rounded corners and updates on an appearance switch). On Windows versions without per-window corner rounding or border colors the default square-cornered system frame is kept — never an error. The window has exactly **one** title bar: ExoSnap's own 40 px band. Windows reserves no non-client area for it, so no native caption is drawn above the product's. The band carries, left to right: the brand mark and the **exosnap** wordmark (the second half in the accent colour), the five nav destinations, the drag handle, the permanent engine-state pill, the notification bell, and the three window buttons. The **selected destination is marked by an accent underline**, not by an enclosing box — words in a 40 px band shared with the window buttons cannot each carry a frame without the band reading as a toolbar. The keyboard focus ring is drawn only while a keyboard user has focus. The window buttons are the last thing that may be given up when the band runs out of room: at the 860 px minimum window the nav tabs compress before Close does. The window's **minimum size is 860 × 700**. A first launch — one with no persisted geometry — opens at a **preferred 1280 × 720**, centred on the primary screen's work area and clamped to it, so a small or heavily scaled display never opens the window under the taskbar or off-screen. This is a preferred size, not a forced one: any valid persisted rect wins over it outright, and the window is freely resizable, maximizable and snappable afterwards. Owning the title bar never costs the window a native gesture. Every gesture Windows offers a normal window works here too: **double-clicking the band maximizes and restores**, dragging the band to the top edge maximizes it and to a side edge snaps it to that half, `Win`+arrow moves and snaps it, right-clicking the band or pressing `Alt`+`Space` opens the **system window menu**, and hovering the Maximize button offers the **Snap Layouts** flyout on Windows 11. Aero Shake and the native resize drag on all four edges and corners behave as they do anywhere else. A maximized window behaves like any other maximized window: it **cannot be resized**, the resize edges answer only in the windowed state, and both the Restore button and dragging the title bar downward return it to **exactly** the size and position it had before it was maximized — including after Windows itself maximized it through Snap, a double-click or `Win`+`Up`. Minimizing a maximized window and bringing it back from the taskbar returns it **maximized**, not windowed. The window is **already at its final position, size and maximized state on the first frame it shows**. It does not appear at one rect and move to another, and a window that was maximized when it was last closed comes back maximized without showing its normal size first. Every nav destination that has a page title states it the same way: the destination's name on the page-title rung, on the same axis as its own content, with that page's controls (the in-depth diagnostics switch, filters) on the right of the same row. Three destinations opt out. **Record** has no page title - its flat context strip names the capture target instead, because the preview is that page's subject. **About** has no header at all, being a single centred identity card. **Settings** has no page title either: its first band is already a toolbar (the preset selector, its dirty badge and the preset actions), so a heading above it repeated the selected navigation tab word for word and pushed the page's real controls a rung further down. The Expert toggle Settings would have carried in that heading rides the right end of the preset toolbar instead. **In-window modal surfaces** — the crash-report consent surface, the recovery prompt and the recording-error surface — share one shape. Every modal surface, this shape and the dialogs alike (source picker, close guard, preset naming), dims the shell behind it with the same scrim: a modal always makes the window recede, never brighten. Their **scrim covers the whole shell including the title band**, because a window that still looks operable behind a modal is lying about what a click will do; the **card itself stays inside the usable content region**, below that band, so a modal never sits on the brand, the navigation or the window buttons. The card is **not drawn as a window**: it has no title bar, no second wordmark and no imitation chrome of its own — the surface names itself with a single eyebrow above its heading, because the real title bar is 40 px above it. Inside the card, chrome, heading and actions are fixed and only the material being read scrolls; anything the user is being asked to **decide** (a consent tick, a "remember this choice") stays visible with the actions rather than scrolling away with the material. **At most one of the three is on screen at any moment, and the one that is up keeps precedence.** A surface asked for while another is showing is **queued**, not dropped and not allowed to supersede: a crash prompt, a recovery offer or a failure report that vanishes mid-read takes its question with it, and none of them can be got back without restarting. Queued surfaces come up one at a time, in the order they were asked for, and each is shown once. Consequently a **recording cannot be started while any of them is up** — a session whose transport is behind a scrim cannot be controlled, and starting one would silently invalidate the offer an open recovery surface is still making. Stopping, pausing and resuming are never blocked: a running recording the user cannot stop is the worse state. --- ## 3. Recording defaults and profiles The built-in default profile is **MKV + AV1 + Opus + CFR 60 fps**. | Setting | Default | |---------|---------| | Appearance | Dark | | Accent | Aqua | | Container | MKV | | Video codec | AV1 (NVENC); on a first start the codec is reconciled once the hardware probe completes, to the best one the GPU encodes (AV1, else HEVC, else H.264) | | Audio codec | Opus | | Frame rate | CFR 60 fps (Default list: 24/30/48/50/60/90/120/144/165/240 fps, entries above the fastest attached display's refresh rate listed but disabled; Expert mode swaps the list for a free-entry field capped at the fastest monitor's refresh rate; an off-list rate gets its own "`` fps (Custom)" entry — see §6) | | Rate control | Constant quality (CQ), quality "High" | | NVENC encoder preset | P4 (all codecs) | | Frame pacing | Phase-correct | | Color range | Limited | | Cursor capture | On | | Countdown | 0 seconds (selectable 0/3/5/10) | | Audio source rows | Order is `APP`, `SYS`, `MIC`. The Settings **Audio sources** card always lists all three rows: the `APP` row is a persisted setting that recedes (inactive meter, explanatory line "Takes effect while a specific application window is the capture target.") whenever a window is not the capture target, and it carries no **Mix into previous track** control because it is the first row. Which sources enter the recording plan still depends on the capture target — an `APP` source exists only while a specific application window is being captured. For screen capture the shipped default is `SYS` enabled and `MIC` present but off. | | Resulting tracks | Each enabled source is a separate resulting track unless merged with the row above | | Webcam | Off | Five read-only built-in presets ship with the app and always appear first in the preset list. They cannot be renamed, overwritten, or deleted; **Save as new…** derives an editable user preset from any of them. | Preset | Container | Codecs | CQ (tier) | NVENC preset | Intent | |--------|-----------|--------|-----------|--------------|--------| | Default | MKV | AV1 + Opus | 19 (High) | P4 | balanced | | Quality | MKV | AV1 + Opus | 16 (Ultra) | P4 | the top of the quality ladder | | Compact | MKV | AV1 + Opus | 30 (Low) | P6 | smallest files, for long screen recordings | | Performance | MKV | AV1 + Opus | 19 (High) | P2 | maximum encoder headroom | | Compatibility | MP4 | H.264 + AAC | 19 (High) | P4 | editing, upload, GPUs without AV1 encode | Each differs from **Default** on exactly one axis. Under constant quality the NVENC preset is an encode-time control rather than a quality one — measured on real gameplay and a real browser scroll, P2 through P7 move VMAF by at most 0.24 points and bitrate by at most 4% — so the quality tier is what separates **Quality** and **Compact**, and the NVENC preset is what separates **Performance**. **Compact** is the one case where a high NVENC preset pays: on dense small text P6 cuts roughly a fifth of the bitrate, because sub-pixel motion search is what that content needs. **Compatibility** keeps CQ 19 because the canonical scale *is* H.264's quantizer scale, so that is the High tier for H.264 by definition rather than by coincidence. The **live configuration is the source of truth**. It is persisted silently and continuously, so the app restarts into exactly the state it was closed in. A preset is a named snapshot the live configuration is compared against: when the two differ, the selector shows `Name (changed)` as a calm hint. There is no Save button, no unsaved-changes warning, and no discard dialog. A write failure (disk full, file locked, …) is not silent either — the change may be lost, so a notification says so, and it offers **Send report**: a failed write is the store's problem, not a setting the user can correct. Capture identity, video bit depth, and HDR mode are **environment facts**, not preset content. Presets neither store nor override them, and a difference in them never counts as a change. Switching presets therefore leaves the capture target alone. The existing H.264 8-bit clamp is the one sanctioned exception. Settings hosts the preset dropdown directly — there is no separate preset manager surface. Built-in presets carry a small **Built-in** badge inside their dropdown option row, so the marker never sits beside the dropdown and shifts the toolbar. The toolbar carries a single visible action button, **Save as new…**, which appears while the preset is `(changed)`. Every other action lives in a `…` overflow menu next to the dropdown: **Rename…** (disabled for a built-in), **Reset** (enabled only while `(changed)`), **Delete** (enabled whenever a user preset is selected, never for a built-in), **Export…**, and **Import…**. **Delete** is the one destructive action in that menu and it asks first: a confirmation dialog naming the preset, defaulting to Cancel, so a single click never destroys a saved configuration. It is deliberately a confirmation rather than an Undo — a deleted preset also moves the selection and re-applies the configuration that takes its place, and restoring all of that after the fact would mean a persisted history the product does not otherwise keep. The Output page carries the same row. Switching presets applies immediately and records a notification-hub entry offering **Undo**, which restores both the previous live configuration and the previous selection. No toast appears — the combo box that performed the switch already offers the way back. Preset names are unique case-insensitively (leading and trailing whitespace trimmed); built-in names are reserved. The naming dialogs reject a collision and let the user correct it; on import a collision is resolved with a numeric suffix ("Name (2)", "Name (3)", …). Preset **Rename** and **Save as new** use a compact ExoSnap overlay card rather than a generic Qt input dialog. Cancel is on the left and the named action, **Rename** or **Save preset**, is on the right; there is no generic `OK`. The existing uniqueness rule controls whether the named action is available. Folder and file selection remain native Windows dialogs, including output folders, preset import/export and one-time export recovery destinations. A failed preset import or export reports itself as a toast: the transfer is started from Settings and is over the moment it fails, so a message pinned to a page would appear where the user is not looking and stay there until they went to dismiss it. Nothing in the live configuration changes, and the toast offers **Send report** for the same reason a failed settings write does. Presets are stored in a human-readable TOML store and can be exported and imported for sharing. Values are validated and sanitized before storage; invalid values are clamped rather than rejected silently, and a damaged store is repaired entry by entry — surviving entries are kept — instead of being reset wholesale. A repair that actually discards something raises a notification; a mere schema upgrade does not. **An application-settings file that cannot be read is never overwritten by accident.** The application settings (appearance, hotkeys, overlays, update channel, window placement, …) live in a separate `settings.ini`. Three load outcomes are distinguished: no file yet (a first run — the defaults are legitimate and are saved normally), a file read successfully, and **a file that exists but could not be read** (corrupt, locked, unreadable). In the third case the session runs on built-in defaults and says so once in the notification hub, and ExoSnap refuses every write it performs on its own behalf — window placement, startup reconciliation, one-time flags — because none of those is the user asking to replace their configuration. The first setting the user *does* change supersedes the unreadable file: it is moved aside to `settings.ini.corrupt` and a fresh file is written, so the old contents are still recoverable by hand. A settings write that fails is reported like any other failed save; it is never silent. --- ## 4. Container / codec / audio matrix The UI offers only **vetted** combinations — never a theoretically-muxable pairing without a tested player/editor matrix. Invalid combinations are blocked before recording starts by a compatibility registry that answers: allowed? recommended? experimental? fallback? warning? | Container | Video | Audio (offered) | |-----------|-------|-----------------| | MKV | AV1, HEVC, H.264 | Opus, AAC, PCM, FLAC | | MP4 | HEVC (`hvc1`), H.264 | AAC | | WebM | AV1 | Opus | Rules and notes: - **MKV** is the flexible default container and the only home for lossless **PCM** and **FLAC** audio. - **MP4** offers only **AAC** audio. Opus, PCM, and FLAC are not offered for MP4; an Opus + MP4 selection is rejected before recording begins. - **PCM in MP4 is deferred (experimental, not user-selectable):** the bundled libavformat emits the `ipcm` (ISO/IEC 23003-5) sample entry, which many players and editors (Windows "Films & TV", QuickTime, several NLEs) do not play. MKV is PCM's home. - **FLAC in MP4** is not a 1.0 target. - **AV1 in MP4 is deferred (experimental, not user-selectable):** the muxer can write it, but the container pairing is not validated against the player/editor matrix, so the compatibility registry classifies it as experimental and the UI never offers it. AV1 ships in **MKV** (the default) and **WebM**. Switching an AV1 selection to MP4 reconciles it to **H.264 + AAC**. - MP4 is delivered by **remuxing the transient MKV to a progressive, faststart MP4** via stream-copy on stop — no re-encode. During the remux the UI distinguishes **"Saving…"** (remux in progress) from **"Saved"** (complete), and the record control reports the remux's **progress as a percentage** once the remuxer has measured one — before that it reads **"Finalizing…"** with no number rather than a zero, because nothing has been observed yet. The remux is cancellable and cancelling keeps the valid MKV. If the remux fails, the playable MKV is retained and the error surfaced. The MP4 carries **BT.709 color metadata**, and HEVC in MP4 uses the **`hvc1`** sample entry (parameter sets in `hvcC`) for Apple/QuickTime compatibility. - MP4 shows a calm informational note that it has lower crash resilience than MKV (the file is only finalized at stop). - **WebM** offers only AV1 + Opus. It must never be paired with H.264 or HEVC. (VP9 in WebM is a possible later addition.) - Exact codec availability further depends on the installed **NVIDIA GPU generation, driver version, the selected container, and the selected video/audio combination**. **Reconciliation rule.** When the user switches container, the selected video and audio codecs are reconciled to a valid combination for the new container (the engine computes the nearest valid combination). Reconciliation is engine logic, surfaced to the UI — the UI does not duplicate it. **HEVC / 10-bit maturity.** HEVC, `hvc1`, and 10-bit encoder paths are functional end-to-end but not yet validated across all NVIDIA GPU generations under live recording. The product advises falling back to H.264 or AV1 if issues appear. --- ## 5. Audio model **Source order and defaults.** The audio source row order is **`APP`, `SYS`, `MIC`**. The Settings **Audio** card always lists all three rows: the `APP` row is a persisted setting, always present and configurable, not conditional on the capture target. While no specific application window is the capture target it recedes (inactive meter, explanatory line "Takes effect while a specific application window is the capture target.") instead of disappearing, and — being the first listed row — it carries no **Mix into previous track** control, since there is nothing before it to fold into. Whether an `APP` source actually contributes audio to the recording still depends on the capture target: it contributes only while a specific application window is being captured. Defaults are **context-aware** (see §3 and "Resolved-decision notes" below): the `APP` row defaults enabled; for screen capture the shipped default is `SYS` enabled and `MIC` present but off. Each enabled source produces its own separate resulting track. **Editable source rows → resolved tracks.** The UI presents editable source rows. The engine resolves the rows into final tracks; the UI never duplicates track-resolution logic. Per-source **gain** and **mute** controls are live and interactive on the **Record page**; the Settings → Audio panel shows the rows as locked previews. **Switching a source off and on during a recording.** The transport's `SYS`, `APP` and `MIC` toggles keep working while a recording runs, like the camera toggle already did. Switching one off does not remove its track: the track keeps running for the full length of the recording and carries silence until the source is switched back on, and switching back on resumes on the next packet because nothing was closed. A track that started at the first unmute would carry a container-level offset, and neither Matroska nor MP4 can express a hole inside a track, so one continuous timeline is the only shape that survives the container and stays trimmable afterwards. A source that was **off when the recording started** was never resolved into a track, so its toggle stays locked for that run. - Per-source **gain** ranges roughly **−60 to +24 dB** (default 0 dB), shown as an "X.X dB" value. - Each row has a **mute** button (labelled **"M"**); a muted source contributes silence. Default is not muted. - The **Mic row hides its gain slider** (mic level lives on the dedicated mic gain control); its mute button is always shown. **Source rows are blocks, not rows.** A source carries four statements -- whether it is included, which source it is, what it is reading, and which track it lands on -- and four statements do not fit in one control slot. The header holds three fixed columns: the switch and the label with its hint on the left, the reading right-aligned in a fixed 68 px column, and the **Mix into previous track** control in a fixed 172 px column. The mix column keeps its width even on a row that cannot offer the control, so the reading column has one right edge across the whole card; the slot is left empty rather than filled with a placeholder dash, and the control in it is not focusable while it is only holding the column open. **The source meter.** The level occupies a line of its own under the header, at the card's full width, with a dBFS ruler beneath it marking -60, -40, -20, -6 and 0. Thirty-two segments here against the Record dock's sixteen: the extra width buys resolution, not fatter cells. The scale IS decibels: the adapter is given the reading and derives the meter position from it, so the bar, the ruler and the number cannot disagree. Zones follow the Record dock on the same scale -- themed up to roughly 72 % of the scale, caution amber to 90 %, error coral above -- so a given loudness reads the same on both surfaces. The loudest segment of the last 1.4 s is held at half strength and then steps down, because a transient that is over before the eye arrives is otherwise unreadable. A source that is producing nothing prints an infinity symbol rather than the bottom of the scale: silence and a level sitting at -60 dB are different facts. **A source that is switched off draws no meter at all** and collapses to its header: a greyed bar the width of the card states nothing while taking as much of the eye as the live one beside it. **Microphone settings are their own card.** `Device`, `Channels`, `Gain` and `Post-processing` sit in a **Microphone** card directly below **Audio sources**, not as rows inside it. They are details of one device, and a card boundary states that without an indent; inside the sources card at equal rank they read as four more sources, and behind an indent they reintroduce the sub-grouping the card layout does not use. The labels drop the word "Microphone" because the card title carries it -- it previously stood four times in a row to make up for structure that was missing. The card's summary line names the selected device, the gain and the processing state, and states `No microphone connected` once there rather than leaving four greyed rows to imply it. **`Mix into previous track`.** It is the third column of the source block's header, in a slot of fixed width that every row reserves whether or not it can offer the control. The control names the track the source joins, not the row's position on screen. `Merge with above` was the first wording; it described where the control sat rather than what it did, and stopped being true the moment the rows were laid out differently (narrow single column, a receding `APP` row, a future reordering). "Previous" refers to the product order `APP`, `SYS`, `MIC` fixed above, which is the model the engine resolves tracks from, so the label stays true regardless of how the rows are drawn. Naming the concrete target instead ("Mix into the system-audio track") was considered and rejected: the target changes with which rows are enabled, so the row's own label would move under the user. The verb is `Mix`, not `Merge`, because that is what the resolver does: the checked source is appended to the track currently being built and its samples are summed into it at that row's own gain, producing one track whose name is its sources joined with " + ". Nothing merges two existing tracks. Three sources chained this way all land in the same track, which is why the target stays "previous track" and does not become "previous source". Checking it folds a source into the track above it instead of producing a separate track, so users can combine sources (for example, system and app audio in one track) without engine-side UI logic. The set of relevant sources is context-aware — it adapts to the capture target. **Mix bus and limiter.** Per-source gain and mute are applied in the mixer; the `APP` and `SYS` rows carry a whole-dB gain field (-60 to +24 dB, Expert) beside the reading, while the microphone's gain lives on its own card. A **brickwall limiter** sits on the mixed bus and is **on by default** at a 0 dBFS ceiling, so summed sources can exceed full scale without hard clipping; the ceiling is an Expert row (-12 to 0 dBFS) shown while the limiter is on. **Microphone DSP chain.** The mic path has a four-stage chain applied in order: **high-pass filter → noise gate → AGC → RNNoise** neural noise suppression. **Every stage is off by default** and toggled individually — there is no master switch — and capture is byte-identical when all stages are off, **unless audio clock slaving has engaged** (measured device-clock drift past ~15 ms, or a measured rate that would reach it within ten minutes); disable *Audio clock slaving* (expert) for bit-exact capture. **Channel / sample-format model.** Output **sample rate** (44.1 / 48 / 96 kHz), **channel count** (mono or stereo), and **bit depth** for lossless codecs (PCM 16/24/32-bit int or 32-bit float; FLAC 16/24-bit) are configurable. Capture itself stays at 48 kHz; the engine resamples/rematrixes **once** after the mix bus. The default 48 kHz / stereo path is a byte-identical no-op **until audio clock slaving engages** (see A/V drift below), after which even the default path is resampled by a sub-audible ppm amount; the *Audio clock slaving* expert toggle restores bit-exact capture. **Opus is locked to 48 kHz.** Bit depth does not apply to lossy codecs (Opus/AAC). Stereo→mono uses an averaging (no-clip) downmix. **32-bit float PCM** is a raw passthrough of the mix bus's native format (no conversion, no clipping headroom needed) and is PCM-only — FLAC has no float mode. It is an Expert switch shown while PCM at 32-bit is selected. The lossy **audio bitrate** field takes the active codec's range (Opus 32–510 kbps, AAC 64–320 kbps), so a value the field accepts is the value the encoder uses. **Opus recording defaults.** Audio application profile, 20 ms frames, complexity 10 when CPU budget allows, VBR/constrained VBR, per-track/channel bitrate. Restricted-lowdelay and 2.5/5 ms frames are expert-only. **FLAC compression level** (0–8, default 5) is configurable; every level is lossless (level only trades encode CPU for file size). Deferred: more than two channels (5.1/7.1), non-vetted sample rates. When resampling is active, the resampler's filter-delay tail is **fully drained** into the encoder before end of stream on a clean stop — no audio tail is expected to be lost. The session report records drained/undrained resampler frames per track; `undrained_frames > 0` is a diagnostic error condition (captured audio that never reached the file), not expected behavior. A failed or timed-out session does not reach the drain and reports the counters as unavailable rather than claiming a clean drain. --- ## 6. Video model **Rate control (canonical model).** "CRF" is x264/x265-specific and is never shown anywhere in the product (UI, tooltip, or API). ExoSnap presents a canonical model mapped per encoder underneath: - **Constant quality (CQ)** — the default (NVENC CQ/CQP under the hood). - **Variable bitrate (VBR)** - **Constant bitrate (CBR)** - **Lossless** — hidden (not grayed) unless the active encoder+codec combination confirms support. A bitrate control accompanies the bitrate-based modes. Switching encoders preserves the selected canonical mode; only the internal mapping changes. Expert rate-control, bitrate, and frame-timing controls sit behind the Expert toggle. **Quality ladder.** In Default mode, quality is chosen from a five-tier ladder, **labelled by tier name alone**: | Tier | Label | CQ | |------|-------|----| | 1 | Draft | 35 | | 2 | Low | 30 | | 3 | Balanced | 24 | | 4 | High (default) | 19 | | 5 | Ultra | 16 | The top tier deliberately avoids "Best" (vague) and "Quality" (collides with the built-in Quality preset's name); the second tier is "Low" rather than "Efficient", so the ladder names quality levels throughout and pairs with "High". The built-in **Quality** preset's CQ 16 lands exactly on the Ultra tier. Expert mode replaces the ladder with the explicit **Rate control** selector (CQ/VBR/CBR) plus a **CQ** spinbox (1–51, no suffix) or a **bitrate** spinbox, depending on the selected mode. The CQ number no longer appears in the Default ladder's labels. It is an ExoSnap scale rather than the value the encoder receives, so printing it beside the tier name presented a product abstraction as encoder mechanics. Expert mode owns the number, and names the quantizer it resolves to underneath the field (for example `AV1 qindex 65 of 255`). **The CQ number is a product scale, not an encoder quantizer.** 1–51 is what the product means by constant quality, and the scale is *defined* to be H.264's quantizer scale, so a CQ a user saved keeps meaning what it always meant. Each codec is then given the quantizer calibrated to that point: | Tier | CQ | H.264 QP | HEVC QP | AV1 qindex | |---|---|---|---|---| | Ultra | 16 | 16 | 16 | 42 | | High | 19 | 19 | 19 | 65 | | Balanced | 24 | 24 | 24 | 94 | | Low | 30 | 30 | 30 | 135 | | Draft | 35 | 35 | 35 | 167 | Values between tiers are interpolated; the ends of the scale are pinned at CQ 1 (AV1 qindex 5) and CQ 51 (AV1 qindex 255). H.264 and HEVC are the identity — measured at the same QP, NVENC's HEVC is within 0.35 VMAF of H.264 at the top of the ladder, better below it, and 22–51% cheaper throughout, which is already what a shared tier name promises. AV1 needs a table rather than a factor: the ratio it needs to match H.264 runs from under 2× at the top of the ladder to nearly 5× at the bottom. A tier therefore aims at the same look on every codec, and AV1 reaches it with a much smaller file: measured on real 1440p60 gameplay, AV1 is never more than 0.9 VMAF behind H.264 at any tier — and 3.3 points ahead at Draft — while using 56–69% less bitrate, and on a real browser scroll the two are within 0.5 points of each other at 6–12% less. The residual is content-dependent and does not vanish: no single quantizer makes two codecs agree on every content class at once. **Adaptive quantization is off.** NVENC's spatial AQ trades bits from detailed regions to flat ones, which is a trade against a bit budget, and constant quality has no budget to trade against. Measured across high-entropy motion and scrolling small text it cost up to 45% more bitrate while scoring lower on both a perceptual metric and a pixel metric. NVIDIA's own preset configuration leaves it off, and recommends it for bitrate-targeted modes. If ExoSnap ever adopts a bitrate-targeted quality mode, AQ is re-evaluated there. **Encoder preset.** An expert **NVENC encoder preset** control (in the Recording format expert section) exposes presets **P1–P7** (P1 fastest, P7 best) uniformly for all codecs; it is never capability-gated (only the recording lock disables it). The **default is P4 for all codecs**. It takes effect from the next recording. **Frame rate and pacing.** The default is **CFR 60 fps**. The Default frame-rate control is a fixed list — **24 / 30 / 48 / 50 / 60 / 90 / 120 / 144 / 165 / 240 fps**. **A rate is selectable only when an attached display can actually feed it**: capture never produces more frames than the screen refreshes, so on a slower panel the entry stays visible but disabled, with a hint naming the fastest attached display's actual rate. Disabled rather than hidden, because a missing entry reads as "the product cannot do this at all" instead of "this hardware cannot". The same rule guards the model: a listed rate above the ceiling never reaches it, since the disabled item state alone only stops interactive selection. Expert mode replaces the list with a **free-entry fps field**, not capability-checked at entry. Its maximum is **the highest refresh rate of any attached monitor, rounded to whole fps, but never below 60**; the minimum is 1. Capture never delivers more frames than the compositor/monitor rate, so a CFR target above it is pure frame duplication. Rounding is to the *nearest* whole fps on purpose: recording 144 CFR from a 143.96 Hz source duplicates roughly one frame every 25 s and never drifts, whereas rounding down to 143 would drop about one frame per second and show as micro-judder. The **60 floor** applies both when no refresh rate can be read at all (headless host, remote session, a driver reporting 0 Hz) and when every attached display genuinely reports something slower — a 30 Hz-only setup still gets a maximum of 60, because the shipped default profile is CFR 60 fps and must stay expressible everywhere. The ceiling is re-evaluated whenever a display is attached, removed, or changes its refresh rate; a configured rate above the new ceiling is then **clamped down** in both the model and the UI, and the clamped value is published like any other change. A user edit is likewise always accepted into the enforced range. Clamping happens **only** on those two occasions: a rate that arrives from outside — persisted settings or a loaded preset carrying a rate from a faster display — stays in effect and is **displayed truthfully** (the Expert field widens its maximum far enough to show it, rather than silently displaying the ceiling). It is brought into range at the next display change or the next manual edit, whichever comes first. When the configured rate is not on that list — after leaving Expert mode, or after loading a preset that carries such a rate — the Default combo grows an extra entry labelled **"`` fps (Custom)"** carrying the real value, placed in numeric order among the fixed entries and selected. The combo therefore never claims a frame rate the recorder is not using. The custom entry disappears as soon as the configured rate is back on a listed value, whether because the user picked a listed entry or because a preset set one; selecting the custom entry itself changes nothing. The current-format footer always shows the true configured value. An expert **"Frame pacing"** control offers **"Phase-correct"** (default) and **"Lowest latency"**. Phase-correct selects frames by present time (it does not blend), so uncapped VRR / high-refresh sources record to smooth, judder-free 60 fps; it does not make 60 fps look like 144 Hz. It is GPU-only and requires no elevation, and applies to **monitor (DXGI duplication) capture only** — window/region (WGC) capture always uses newest-at-tick. VFR output is unaffected. When VRR/CFR judder is measured while in Lowest latency, Diagnostics recommends switching to Phase-correct via a fix action. The CFR timeline stays true to the wall clock. If the encoder cannot keep up in real time for a sustained stretch, the recorder **skips** the output frames it could never have encoded in time — counting them as **real** frame drops (part of the same real-drop count a caution toast reports) rather than letting the video media time fall behind the audio and silently compress the recording. The result is an honest, correctly-timed file with a visible drop count, not one that plays back out of sync. A **VFR** recording's timeline likewise never starts before the recording did: on monitor capture, the first frame can carry the display's last real present timestamp from before Record was pressed (a desktop that sat static reports its last repaint), and that stale origin is clamped to the session start. The file's duration therefore matches the real recording time — a static source before recording never inflates the lead-in — and the video timeline shares its origin with the published A/V epoch. **Bit depth.** 8-bit for all final codecs; **10-bit (P010)** is available for HEVC Main10 and AV1 where the GPU supports it (H.264 stays 8-bit only). The freely-choosable Expert **Bit depth** row is **SDR-only** — higher precision, no HDR transfer curve or wide gamut — and only appears once the selected codec carries 10-bit at all (HEVC/AV1); for H.264, which has no 10-bit path, the row is not shown rather than shown-and-disabled, so the expert view lists only choices that apply to the current codec. This is independent of native HDR10, which pins 10-bit unconditionally regardless of this setting (§HDR below) — "10-bit" as a concept is not SDR-only, only this particular expert toggle is. **Chroma.** **4:2:0** is the default and is universal (all codecs, 8- and 10-bit). **4:4:4** is an Expert-mode option available for **H.264 and HEVC at 8-bit** (NVENC High 4:4:4 Predictive / HEVC Range Extensions) on GPUs that report YUV444 encode support; it keeps full color resolution (sharper text/UI) at the cost of larger files. **4:4:4 is not available for AV1** (NVENC AV1 is 4:2:0 only), **not available at 10-bit**, and not available with native HDR10. The Expert **Chroma subsampling** row itself only appears when the selected codec **and** the active GPU can carry 4:4:4 at all; for AV1, or for a GPU whose probe reports no YUV444 encode support for the selected codec, the row is not shown. When the row is relevant but a 10-bit selection is the sole conflict — a choice the user can resolve right there by switching Bit depth back to 8-bit — the row stays visible with the 4:4:4 item disabled and an explanatory hint instead of hiding, and an invalid stored selection is reconciled back to 4:2:0. **4:2:2 remains unavailable** (the NVENC generation has no 4:2:2 path). While a recording runs in **4:4:4**, the **live preview stays available** (it shares the composited RGB frame with the preview before the AYUV conversion — see the live-preview note in Section 7), and **frame snapshots stay available** as in 4:2:0: the CaptureFrame hotkey reads back the packed AYUV encode surface and decodes it on the CPU with the exact inverse of the encoder's RGB→AYUV conversion (same BT.709 matrix and Full/Limited range as the recording). **Color range and metadata.** **BT.709 color metadata** is written to every MKV and MP4 output. The **Y'CbCr color range** (Full or Limited) is selectable behind Expert mode and is valid for every codec/container combination (never gated), with **Limited** as the default. Because some players (notably VLC) ignore the range flag and always expand limited→full — making Full-range recordings look crushed/dark there — Diagnostics surfaces a compatibility tip with a one-click Full→Limited fix when Full is selected. Presets carrying an older Full default are auto-migrated to Limited; an explicit Full is respected as a deliberate opt-in. **HDR handling.** HDR-capable displays are **detected automatically**; detection is not a setting and cannot be turned off. Once an HDR-active display is detected, an **expert-only HDR handling control** (Settings → Recording format section) chooses the outcome: - **Tone-map to SDR** (default) — the safe, universally compatible choice. - **Record native HDR10** — keeps the original PQ / BT.2020 HDR10 signal. The **HDR handling** row itself only appears once at least one probed display is HDR-active. On an all-SDR system there is nothing this control could meaningfully change, so the row is not shown at all rather than shown-and-inert; it appears the moment an HDR-active display is detected and disappears again if none remains. A stored `hdr_mode` is never rewritten while the row is hidden — it takes effect again exactly as before the moment a qualifying display returns. Detection is not a single reading taken at launch. Windows HDR is a system-wide toggle the user can flip at any moment, so the per-display facts are re-read on every display-configuration change, and the recording-admission check reads them fresh regardless — a desktop switched to HDR after ExoSnap started reaches the HDR handling row and the Diagnostics HDR card rather than waiting for a restart. Behavior: - On a non-HDR display the choice has no visible effect either way (the row is hidden there, per above). - **Record native HDR10** is only selectable with a codec that can carry it (**AV1 or HEVC**). With H.264 the option is disabled and a calm inline note explains why ("Not available with H.264 — switch to AV1 or HEVC"). This is never shown as a warning/error state, and switching the codec updates availability immediately. - If HDR10 recording is selected and the codec is later changed to H.264, the setting is **not** silently reset. The conflict is caught by the diagnostics readiness gate, which **blocks recording start** until it is resolved (switch codec, or choose Tone-map to SDR). The blocker carries a codec fix action ("Switch to AV1" / "Switch to HEVC", availability-aware). It fires only when HDR10 mode is selected, the codec cannot carry HDR10, and the display's HDR is currently on; H.264 + SDR tone-map is explicitly not a conflict. - For native HDR10, the pipeline pins **limited range** and **10-bit**, writes HDR10 metadata **both at the container level** (MKV Colour / MasterMetadata, MP4 colr/mdcv on remux) **and in-band in the bitstream** — HEVC Mastering Display Colour Volume (SEI 137) messages, AV1 HDR MDCV metadata OBUs, emitted on every keyframe so players that ignore container-level HDR metadata (notably some Apple players) still receive it. **Content light level (MaxCLL/MaxFALL) is deliberately not written**, in either place: those values describe the brightest content in a finished piece, and a live recorder does not know them while it is still recording. A wrong MaxCLL makes a player tone-map against a peak the file never reaches, which is worse than the absent-and-ignored value. The on-screen monitoring preview is an SDR approximation of the HDR signal. - SDR overlay sprites (webcam PiP, cursor) are placed at the captured display's Windows SDR-content brightness level (`DISPLAYCONFIG_SDR_WHITE_LEVEL`) so the PiP matches SDR windows on the same screen; 203 cd/m² is the fallback when the level cannot be read. The level is sampled once when the recording starts — moving the Windows SDR-brightness slider afterward does not retune an active recording. - **Advanced Color Management (SDR desktop, HDR off):** with Windows' automatic color management enabled, the desktop composites to scRGB FP16 even though the display stays in SDR mode. Such a desktop carries SDR content (reference white = 1.0) and is recorded by encoding it with the sRGB transfer function — it is **not** tone-mapped, and it records in every HDR-handling mode, including `Off`. Only a display that actively reports an HDR color space is treated as HDR. Recorded output therefore matches the live preview and the desktop. - **HDR scope for 1.0:** HDR handling (both tone-map-to-SDR and native HDR10) applies to **monitor (duplication) capture** and to **window/game capture** (Windows Graphics Capture). When the window's hosting display is HDR-active and HDR handling is on, WGC negotiates a scRGB FP16 frame pool and feeds the same tone-map / native-HDR10 machinery as the monitor path (so the same H.264+HDR10 blocker and expert control apply to a window on an HDR display). The hosting display is resolved once at recording start; moving the window to a different monitor mid-recording keeps the session's initial HDR decision. There is still no HLG or wide-gamut generalization beyond BT.2020. --- ## 7. Capture targets and webcam Three capture targets: - **Monitor / display** — captured via DXGI Output Duplication. - **Application window** — window capture (Windows Graphics Capture path). - **Screen region** — a rectangular region, with a refined region-selection overlay and a live cropped preview. **Window-capture update rate.** Windows Graphics Capture delivers frames on repaint and applies its own minimum interval between them, which defaults to one display refresh and caps a window recording below the selected frame rate on a high-rate target. Every ExoSnap capture session therefore requests a minimum update interval of **1 ms**, so the delivered rate is bounded by the source and the encoder rather than by the capture API. The value Windows actually accepts is read back and logged alongside the requested value and the session's target frame rate. On a Windows build without the setting the session continues at the OS default and logs that fallback as a warning — the recording still runs, but its window-capture rate may stay at display refresh. Any other failure to apply or read the interval fails the recording rather than silently recording at an unknown rate. Cursor capture is a toggle (on by default). Single-frame capture (a "capture frame" action) is available during recording via an on-screen dock control and a hotkey. **Source picker.** `Change source` opens a modal application surface with three tabs: **Displays**, **Windows**, and **Region**. Displays and Windows use selectable cards with a cached still thumbnail, source-kind glyph, resolved target label, and an explicit selected state. A missing thumbnail uses a stable placeholder rather than collapsing the card. Cards are fully clickable, keyboard focusable and accessible; double-click or Enter confirms the focused card. A fixed footer contains Cancel and the named confirm action. The Windows tab reports its result count, offers search, and uses a visible scrollbar whenever its cards overflow. Cards reflow from two columns to one at the narrow layout. A selection survives refresh when the same target identity remains. If it disappears, the picker says so and leaves the selection unresolved rather than silently choosing another target. **Thumbnail freshness.** Thumbnails refresh on their own; the picker offers no manual refresh control. Only the cards inside the scrolled viewport are refreshed, one target at a time on a fixed interval, so the cost is the same whether two cards or forty are on screen and the perceived refresh rate rises as fewer cards compete. A closed picker refreshes nothing. A target that stops being capturable -- minimized, cloaked, behind the secure desktop, or on a display that went to standby -- keeps its last thumbnail, shown dimmed, and is never reverted to the placeholder: the card's geometry does not change and the grid does not flicker. Only a target that has never delivered a thumbnail shows the placeholder. Thumbnails are held in memory for the session and are never written to disk; a target that disappears loses its thumbnail with it. The Region tab puts **Draw custom** first and gives it the strongest emphasis, followed by **16:9**, **9:16**, **1:1**, and **4:5** presets. Draw custom enters an immediate snipping-style drag overlay. A preset creates an editable starting rectangle rather than committing a fixed crop. The rectangle, dimension label, and resize handles remain visible and editable until recording starts; recording hides and locks them. Region labels retain the distinguishing part of the display name when space is tight instead of eliding every row to the same prefix. **Record preview box (content-fit).** The Record-page preview box follows the **current source's aspect ratio** — width-driven and vertically centered in the available space (height-clamped and horizontally centered for sources taller than the area) — so **the edge of the box is the edge of the recording**: there are no letterbox/pillarbox fill bars inside the box, and genuinely black recorded content is distinguishable from padding. The box carries a **1px neutral structural line on its edge** in every state. State is communicated by the local status pill, timer and actions, never by turning the largest surface into a status light. The box follows aspect changes live: switching targets, changing a region, or the captured source changing its own dimensions re-fits the box. With no source selected (or before the source's dimensions are known) the box keeps its default full-area size. The preview's meta/stats text rows (top target line, bottom stats/format line) are **on-screen indicators drawn over the video — and over the webcam PiP — in both the live (DXGI) and static preview paths**; they are never part of the recording or of frame screenshots. **Idle live preview.** Before recording, the Record-page preview of a **display** is fed by the same DXGI Output Duplication backend the recording uses, owned by a shared capture hub: the preview is VRR- and HDR-true, shows no OS capture indicator, draws the live cursor, and **holds its last frame through a monitor unplug/replug** instead of blanking — production resumes when the display returns. The capture exists only while the preview is visible and is closed with it. Window and Region previews run their own Windows Graphics Capture of the selected target (see KNOWN_LIMITATIONS for the exact boundary). **Preview presentation is producer-driven, and never owes a frame.** The preview redraws when the capture producer publishes a frame, not on a timer and not at display refresh — an idle desktop costs nothing. The guarantee that comes with that: **if a frame has been published and the window could not render it — it was moving between monitors, it was unexposed, its scene graph was being rebuilt — that frame is presented as soon as the window can render again.** No second frame and no unrelated interaction is needed to release it. In particular, moving the window from one display to another never leaves the preview frozen until the mouse moves. This holds for the idle preview and for the live preview during a recording alike. **Ready-state dock gating.** The single-frame capture (screenshot) button reads a frame back from the live preview, so in the Ready state it stays disabled until that preview has actually rendered its first frame. The Record-dock microphone toggle is likewise non-interactive while no audio input device is present, showing the tooltip `No microphone connected`. **Live preview (WYSIWYG during recording).** **Once recording starts, the preview shows exactly the frame the engine is encoding** — the composited, pre-encode source (cursor and webcam PiP already baked in) is shared to the preview through a GPU texture, and the preview's own capture stops. There is no second capture running alongside the recording, and the preview reflects what is actually being recorded (so a black-screen or swap-chain problem is visible in the preview, not hidden by an independent capture). During the pre-record countdown the preview holds its last live image until the first recorded frame arrives, so there is no black flash. This includes **native HDR10** recording: the engine shares its HDR frame and the preview tone-maps it to SDR for display, so what is shown is the recorded frame, rendered the way SDR players will approximate it. The one exception is the rare already-PQ 10-bit desktop, where no shareable frame exists and the preview keeps its own capture (see KNOWN_LIMITATIONS). **Webcam PiP.** Webcam is not a fourth capture-target tab. Its device lives in Settings > Webcam, its session enable control lives in the Record transport, and its position and size are adjusted in the preview. The picture-in-picture overlay is **composited into the recording** (it is an in-video element, not an on-screen-only overlay) and rendered WYSIWYG with a real mirror option and a selectable overlay placement. In a constant-frame-rate recording the PiP keeps moving at the encode cadence even while the desktop is perfectly still: a screen capture only produces a frame when the screen changes, so the held screen is composited again with the current camera image rather than repeating the previous composited frame. During a capture-loss recovery the picture is held frozen instead, until the capture source is reopened. On the rare HDR10 display whose desktop frames arrive already in PQ, the engine cannot composite overlays and records **without the webcam and cursor**. When that happens the Record preview drops its picture-in-picture to match the file, and a notification says so. The preview never shows an overlay the recording will not contain. Webcam opacity (0–100%, default 100%) is applied identically in the Record-page preview and the recorded output; it is a live control on the Settings → Webcam card. The mirror is a live, UI-editable control and its preview matches the file. The chroma key composites the same way: the live preview uses the identical key color, tolerance, softness and spill reduction the encoder uses, so a green screen that looks clean while setting up is the one that lands in the file. Those chroma parameters — an on/off toggle, a key color (Green, Blue, Magenta or a custom picked color), and tolerance, softness and spill-reduction sliders — are editable on the Settings → Webcam card in a group that stays collapsed until chroma keying is turned on. Opacity and every chroma parameter are pushed live while recording (they never require a restart), the same class as the mirror; only the camera device and resolution are fixed for the duration of a recording. Its on/off is a single control surfaced in two always-in-sync places — Settings → Webcam and the Record-page transport dock (camera button) — and is off by default. Turning it on both includes the webcam in the recording and starts the live setup preview; the camera opens only while it is on (opening Settings → Webcam no longer turns the camera on by itself). The webcam is never recorded without a selected device (the first available camera is pre-selected when one exists). By default the picture-in-picture starts in the bottom-right corner of the preview with a small margin from both edges (it is not flush to the corner); it can then be moved and resized anywhere **within the displayed video area, right up to every edge**, and the preview renders it **exactly as it will appear in the file — never squished, trimmed or clipped**. The preview's meta/stats text rows draw above the picture-in-picture as on-screen indicators (they are not part of the recording), so a bottom-edge placement is shown in full underneath them. With **no camera attached** the Record dock's webcam control is unavailable and says so; attaching one makes it available again. Unplugging the last camera never loses the stored choice — it returns when the device does. When the toggle is on but the selected camera cannot be opened, the dock button shows a coral error state and its tooltip names the reason (`Camera can't be opened — `). Recognised failures read as a plain-language sentence with the raw diagnostic in parentheses (e.g. the camera offers no compatible video format, or is in use by another application); unrecognised failures show the raw reason verbatim. Turning the webcam off clears the error, so re-enabling it starts neutral until a new open failure actually occurs. The device, resolution, mirror, opacity and chroma-key controls stay editable regardless of the on/off state (opacity and the chroma-key parameters are live-adjustable on the Settings → Webcam card, even mid-recording). The webcam is the only feature that depends on Windows Media Foundation: on Windows N/KN editions without the Media Feature Pack, the app still launches and records normally, but the webcam UI is disabled with a notice referencing the Media Feature Pack and a "Webcam (MF)" row appears in Diagnostics. **Capture-exclusion.** ExoSnap's own on-screen overlays (recording status, diagnostics, countdown, quick-control pill) are drawn with `WDA_EXCLUDEFROMCAPTURE` so they are visible on screen but not in the recorded frame. They are also click-through. If capture exclusion cannot be guaranteed on a given system, the overlay hides itself for the session rather than risk contaminating the recording. **Anti-cheat posture: no injection.** ExoSnap does not inject into or hook other processes, and does no memory access, to capture — it uses OS-level capture APIs (DXGI duplication, Windows Graphics Capture) only. Overlays are never auto-disabled; the user gets a global opt-out for on-screen overlays plus a one-time, non-blocking banner: "Anti-cheat detected — ExoSnap overlays do not inject; disable overlays if required by the game." The optional PresentMon-based present/tearing observation is an **in-process** ETW consumer (a real-time trace session on a worker thread, ADR 0033), opt-in and elevation-gated, never a hard dependency; the app degrades gracefully when not elevated. Three states are reported and are distinguishable from each other, because "no measurement" has three different causes the user can act on differently: `requiresOptIn`, `requiresElevation`, and available. The opt-in is **session-scoped** — off at every start, on for this session only, never persisted (§11). Turning it on never prompts for elevation. It offers instead: a toast whose action performs the restart, refused by a running recording and safely declined at the UAC prompt (§11). **Known target-identity boundaries.** A saved Display or Region target is remembered by a hardware-stable identity (the monitor's device path plus its EDID vendor/product, and its serial number when the panel reports one), not the GDI device name. Saved monitor targets therefore survive unplug/replug, driver restarts, and reboots in a different port order, and never silently select a *different* physical monitor. When the saved display genuinely cannot be found — it is unplugged, or two identical monitors with no serial number were cable-swapped — the app does not guess: it shows a calm "Saved display not found" notice and leaves the source unselected until you choose one (it is then remembered). Region rectangles are stored relative to their anchor display, so a resolution change carries the rectangle proportionally rather than pixel-exact. **Device loss mid-recording (ADR 0046).** Losing a capture device during a recording is handled per device type — a coherent, honest policy rather than a blanket "stop and restart". The recording is never retargeted onto a *different* device; the engine only holds or reacquires the **same** source. - **Display (monitor).** A brief loss (mode change, sleep/wake, reconnect) holds the last frame and reopens the same display (by GDI name), indefinitely, so the recording continues seamlessly. A GPU removal ends the recording cleanly (the segment stays valid). - **Window.** Closing the captured window ends the recording cleanly — a closed window is gone for good and is not retargeted. (Merely minimizing or covering it keeps recording the last frame.) - **Audio source.** An audio endpoint lost mid-recording (a mic unplugged, a headset switched, the system output changed, the audio service restarted) **no longer ends the recording**. The affected source goes to honest silence and the recording keeps running — video and every other audio source are untouched — while the engine reactivates the source with the same identity every 500 ms. In a merged track only the dead source's contribution falls silent; the others keep mixing. If *every* source of a merged track is lost at once, the track keeps running on exact-length silence for as long as the outage lasts — reopening the endpoint included — so its duration still matches wall time and it never slides against the video, and the first source to come back re-enters at the track's current position (no repeated audio, no backwards timestamp). A fixed-device microphone reacquires that exact device; the default microphone and the system-output capture follow the *current* Windows default (they represent "the system default", not one pinned device); an app/window audio capture keyed on a process reacquires only while that same process is still running (it never grabs a different process that reused the PID) and otherwise stays silent. The silence gap is real and shown: a calm live notice in Diagnostics while a source is degraded, a standing notification for the duration of the outage (updates in place if the degraded set changes, clears the moment every source reactivates or the recording ends), and the post-flight report notes that the recording contains a silence gap. - **Webcam.** Losing the camera freezes the last picture-in-picture frame and reopens the same device, indefinitely; the recording continues. If every audio source is lost at once, the recording continues **video-only** (the picture is the primary source). The engine never auto-switches an audio source to a freely-chosen *other* device. **The captured source changing size mid-recording ends the recording.** The encoder, the compositor and the colour pipeline are all fixed at the source size resolved when the recording started, so a source that changes its own pixel dimensions mid-session cannot be followed. The recording ends with the explicit error `capture source size changed during session from to ; restart recording to reconfigure encoder`, and the file written up to that point stays valid and playable. Resizing the captured **window** is the everyday case — including dragging it onto a display with a different scaling factor, which changes its captured size; a **display** whose mode change alters its resolution ends the recording the same way (a mode change that keeps the resolution, like a refresh rate switch or sleep/wake, is the hold-and-reopen case above). Recording again picks up the new size. Recording a moving or resizing window is not otherwise restricted: the recording follows the window around the desktop for as long as its size stays the same. **Displays are numbered sequentially everywhere.** The internal GDI names skip numbers after plug/unplug cycles (`\\.\DISPLAY6`, `\\.\DISPLAY7` on a two-display desktop); the user-facing "Display N" label re-sequences the attached displays to 1, 2, 3… and the source picker, the Record header and the output filename all use the same numbering. A display that just left the topology falls back to its raw number rather than borrowing another display's. **Target labels have one C++ resolver.** The picker, Record context, Recent menu, output naming and notifications consume the same resolved label instead of reconstructing one in QML. Application labels prefer a stable app name plus a concise document or page title when both add information, for example `Brave - Claude Design`. If no distinct app name exists, the raw window title is used, for example `Task Manager`. Identical parts are never repeated. **Fullscreen capture matrix.** How a target behaves depends on the app's presentation mode: - **Windowed** and **Borderless / windowed-fullscreen (FSO, flip-model)** record correctly on **every** target — Monitor, Window, and Region. This is the common case on Windows 10/11; most in-game "fullscreen" settings run as borderless/flip today. - **Legacy exclusive fullscreen (FSE)** bypasses the desktop compositor. **Window** capture records a **black or frozen** frame — ExoSnap cannot capture an FSE window in isolation (hook/injection capture is deliberately rejected; see §14). **Monitor** (and Region, which is monitor + crop) capture *can* record exclusive fullscreen. - **The honest rule:** to record an exclusive-fullscreen game, capture the **monitor**; to capture a **window** directly, run the game in borderless / windowed fullscreen. ExoSnap detects an FSE window target **pre-flight** (`rec.capture.exclusive_window`, §11): a proven-black window is a Blocker and stops the start, and the check offers a one-confirm "Record the monitor instead" fix. A game that changes the desktop resolution while recording a monitor ends the recording cleanly with an explicit size-change error (the footage up to that point stays valid). **A window that stops producing frames *during* a recording** — the shape a mid-session switch into FSE takes — is reported as a **capture stall**, not as a diagnosed cause. What ExoSnap measures is the absence of frame progress: while a **window** capture is recording, it watches the count of frames the capture backend actually produced. If that count does not move for **10 seconds** and the window is fullscreen-shaped, alive, visible and not minimized, a standing **caution** notification appears — *"Window capture appears to have stalled. … The recording is still running, but the captured window may be frozen."* The recording is **never** stopped automatically: the rest of it may be worth keeping and the source may recover. The notice is raised **once** per stall, is cleared the moment frames resume (a later independent stall raises a new one), and a session that had one records `window_capture_stall` in its session report. **A display or region capture** is watched by the same clock. A desktop nobody touches is legitimately silent for minutes, so starvation alone stays a log line; the one corroboration that turns it into the standing notice is a **console display that is off or asleep** (Windows' display power state): duplication opens on such a display and never presents. The notice then reads *"Display capture appears to have stalled. … The recording is still running and holds the last picture. The display is off or asleep; wake it, or stop the recording."* Either way the pipeline card stops saying *Good* once the source has produced nothing for 10 seconds: the emitted rate stays at target through any stall (the pacer repeats the held frame), so the card's reason names the quiet source instead. Only the stall is claimed. Exclusive fullscreen is named only when a fullscreen signal (the Shell's QUNS state or a PresentMon `ExclusiveFullscreen` observation) actually corroborates it, and even then as a conditional suggestion — never as *"exclusive fullscreen detected"*. **What this does not cover:** an *ordinary* (captioned or non-monitor-filling) window that stops producing frames stays silent, because mid-recording nothing distinguishes it from a window that simply has nothing to redraw, and a false alarm on an idle text editor is worse than silence. A minimized, hidden or virtual-desktop-cloaked window is likewise silent — it is supposed to stop. Detection is also a **notice, not a repair**: the frames already lost to the stall are gone. The trade-off runs the other way too, and is deliberate: a **fullscreen-shaped window whose content is genuinely at rest** — a borderless video player left paused for ten seconds — produces no frames either and raises the same caution. That is why the wording is *"appears to have stalled"* and *"may be frozen"* rather than a verdict: for those ten seconds the recording really did hold one frame. The notice clears itself the moment the content moves again. *(Cells requiring a real legacy-FSE title are verified live before being promised as behavior; the matrix above reflects the shipped detection + monitor path.)* --- ## 8. Recording lifecycle **Pre-flight readiness gate.** Before a recording starts, all blocker and notice checks run and are surfaced green/amber/red with fixes. **Recording start is blocked while any diagnostic blocker exists** (for example: no supported NVENC encoder detected, hard-stop disk threshold reached, or an unresolved HDR10-vs-H.264 conflict). If no supported NVIDIA NVENC encoder is detected, recording is blocked with a diagnostic message rather than silently falling back. Blocking is a property of the **start path**, not of the Diagnostics page: every blocker is enforced where the recording is actually admitted, so a blocker holds even if the Diagnostics page was never opened. The start is refused with the blocker's own reason — never a generic failure. **No source is not Ready.** When no capture source is selected, the shell and Record status read `No source` or `Unavailable`, the preview placeholder explains that a source must be chosen, Record remains visible but disabled, and a separate **Choose source** action opens the picker. This expected empty state raises no first-run toast. A source that disappears is likewise never represented as Ready and is never silently replaced. **Page composition.** The Record page is a flat **context strip**, the separate **preview**, and the **transport dock** on one rhythm, with the shared 24 px page inset and stable gaps. The context strip has no card background, outer frame or shared border with the preview. It carries, left to right, a capture-kind glyph and the shared resolved source name; at the right, the resolved format summary, **Recent**, and **Change source**. Recent shows text plus a clock glyph when space permits and keeps the clock-only accessible action at minimum width. Its menu uses the same target labels as the picker. At the 860 px minimum the strip stays one line: the format summary yields first, while source identity and access to Recent and Change source remain. The context strip and transport preserve their height and action slots in every lifecycle state. Controls locked by a running recording stay in their slot with a readable reason and accessible description rather than moving neighboring content. The preview is the page's subject and takes all remaining height. It keeps the source's aspect ratio. Its border is **structural and neutral in every state** - it says "this is one bounded surface", never what the engine is doing. It used to take the state's colour, which turned the largest object on the page into a roughly 1 000 px status light repeating what the state pill in its own corner, the shell's status pill and the transport's one recommended action all already said, and left "success" and "failure" differing only by the hue of a ring half a screen wide. Over the frame sit the state pill (top left) and, while recording or paused, the live readout (bitrate, drops, drift, output size) on the same ground as that pill. Nothing that changes with engine state is placed *around* the frame, so it never resizes because a message appeared beside it. **State is said locally, and once.** Each state names itself through the status pill over the frame and in the title band, through the transport's one recommended action, and — only where something is unresolved — through a modal overlay or a toast (see **No page notice**). The **LOCKED** badge is a neutral statement of fact, not a caution: the capture setup is locked for the duration of a recording by design, so there is nothing for the user to attend to. **State colour vocabulary.** Coral means recording, destructive or error; amber communicates attention, including both warning and the deliberately held **Paused** state; green means ready or success; the accent means selection, focus or primary interactive emphasis; everything else is neutral. Paused uses the semantic `paused` role in the status pill, timer, on-screen overlay, tray and taskbar. It is distinguished from a warning by its pause glyph or `Paused` label. **Resume stays accent**, because it is an action rather than a state. Warning uses the separate semantic `warning` role and a warning glyph or explanatory copy. Countdown, Preparing, Stopping, Saving and Locked are quiet neutral states. Cancelling a countdown is not destructive and is drawn as the accent-filled primary action of that state, told apart from Record by its glyph rather than by its colour. **Transport dock.** The Record page's controls sit in one bar along the bottom, in three groups of deliberately different weight. The **dock is the recessed base and its controls sit on it** — a raised bar with darker controls read as holes punched into the transport, which made the thing meant to be pressed the darkest thing on the page. - **Left — the sources.** One round icon button per audio/video source (`APP`, `SYS`, `MIC`, camera), each carrying its live level as the button's own ground, rising from the bottom. The fill is themed up to roughly 72 % of the meter scale, caution amber from there to 90 %, and error coral above it; the zones sit on the meter scale rather than on the drawn height, so a given loudness always has the same colour. These are icons, not labels: they are the most-used controls on the page and the four abbreviations that preceded them said nothing about what they toggled. Every one has a tooltip and an accessible name spelling the source out in full. A source whose device is merely unusable right now (a camera that will not open) stays visible and disabled with the reason in its tooltip; a source with **no device attached at all** is not shown, because a control that can never act and has no reason worth reading is noise. - **Centre — the elapsed time**, the largest element on the page. It is neutral while idle, coral while recording and the semantic paused amber while paused, using tabular figures so the digits do not shift. - **Right — the actions**, in two tiers: the per-recording secondary actions (capture frame, add marker, split, pause) as round icon buttons, then the one recommended action as a wider pill. There is never more than one **filled** pill, and a filled pill means *something is happening now*: **Stop** while recording, **Resume** while paused, **Edit** once a recording has finished, and **Cancel** during a countdown. While a recording is **paused**, Resume holds the fill and Stop keeps its size and its coral but gives up its fill, so the bar still answers "what now?" with one control. **Record is an outlined pill, not a filled one**, and carries no glyph beside its label. The idle bar has nothing in flight to announce; a solid accent slab there made the transport shout about a recording that has not started, and the record dot beside the word *Record* only restated the label. The countdown is the exception and keeps the fill, because then something *is* happening and cancelling it is the one available action — which is also why Cancel is the one state that still draws a glyph, so it is never read as Record at a glance. After a successful recording the recommended action is **Edit**, because opening what was just recorded is what the product is for and starting the next one is not. Record is **not** shown beside it: the way out of a finished run is a round **back** glyph that returns the transport to its idle arrangement, where Record lives. The round slot before it becomes a **folder** glyph that reveals the recording in Explorer — the one thing a user reaches for between finishing a take and deciding what to do with it. **Capture frame is hidden** in this state: it is an action *on* a recording in progress, and beside a finished one it offered to photograph nothing. Nothing is undone by it — the recording stays exactly where it was written, and the manifest and the result are untouched. Until that glyph existed the only exit from Completed was to start the next recording, which made "I am done looking at this" and "record again" the same button and left two text pills on a bar the rest of which is round glyphs. Edit is hidden, not disabled, when the recording cannot be edited at all (a split recording, a missing file, a failed run): a permanently dead button next to a successful result reads as a defect — the back glyph is still there, so the state is never a dead end. It is never a separate control floating between the Preview Surface and the dock — the page is one Preview Surface, 16 px, one dock. **No page notice.** The Record page carries no inline banner. Everything such a banner used to say is already said by a surface that does not move the page: a **modal overlay** for a condition the user has to answer, and a **toast** for an event that is over. Both compose above the page instead of taking space out of it. That is the whole reason. The preview is the page's fill-height element, so anything appearing above it comes straight out of its height and, through the aspect-ratio fit, out of its width as well. Measured at 1 600 x 1 000, a single banner line moved the Preview Surface down 60 px and narrowed it by 106 px. A user who has watched that frame for a whole recording should not have the composition jump to announce something another surface is announcing at the same moment. The banner survived longest for failures, on the grounds that a failure is unresolved and stated nowhere else on the page. That reason no longer holds: a failed recording raises the modal failure surface with the same sentence plus the detail and the report action, so the banner repeated it and shrank the preview WHILE the modal was on screen. Every other producer had a second surface too -- a source that is gone is what the preview placeholder already asks about, with the picker attached; a settings write that failed, a degraded audio source and a refused quick action each raise their own toast. Where each condition is stated now: | Condition | Surface | |---|---| | Recording failed | Modal failure surface, with detail and the report action | | Capture source gone | Preview placeholder, with **Choose source** | | `Blocked` (including an unwritable output folder) | Toast naming the blocker, with **Open Diagnostics** or **Change folder** | | Audio source degraded mid-recording | Standing toast, replaced in place as sources degrade or recover, taken back when all recover | | Region too small, display gone, refused split | Toast, because the action simply did not happen | | Settings write failed | Toast | | Recording saved | Toast with the file name and, when the session ledger observed one or more problems, "N problem(s) observed" and **View diagnostics**; otherwise just the file name, with **Open folder** | | Frame saved | Toast, with the file name and **Open folder** | | In-depth diagnostics turned on in a standard process | Toast, with **Restart as administrator** (§11) | No toast is raised for a measured problem while a recording is in progress — interrupting the thing being recorded to report on it is the one place a notification cannot go. The notification hub records the problem silently instead, and the count first reaches the user in the "Recording saved" toast above. **Stopping a recording must not change the Preview Surface's bounds**, and neither must failing to start one. **Round-control states.** Every round dock control reads the same way in every state, and the four states are told apart by more than one cue: | State | Reads as | |-------|----------| | Available, off | Raised surface, hairline border, secondary ink | | Hover | One step brighter surface, stronger border, full-strength ink; no movement | | Active / on | Accent-tinted surface **and** accent border **and** accent icon | | Unavailable | Drops to the dock's own fill (no longer raised) and to the dimmest ink, keeping its hairline so the control is still visibly there; no hover or press state | Unavailable and merely-off must never look alike. The earlier treatment moved only the icon colour, which made "no microphone attached" and "microphone switched off" identical. **Reason tooltips.** Every icon-only dock control has a tooltip naming it, and when it is unavailable the tooltip carries a second line saying **why**, in the product's own words — the camera's actual open failure when there is one, "no camera was detected", "no microphone was detected", "the capture setup is locked while a recording runs", "the preview has not produced a frame yet", "a manual split needs an MKV or WebM container". A reason is never invented when a real one exists. The reason is also the control's accessible description, and it is reachable by hover even though the control cannot be activated. **Preparing.** Between the trigger (or the end of the countdown) and the running recording, the app briefly shows a **Preparing** state while device setup — display facts, webcam open, and the capture lease hand-off — runs off the UI thread, so the window stays responsive rather than freezing at the moment of the click. The default countdown is 0 s, so for most starts Preparing is the first feedback the user sees. Pressing the record hotkey during Preparing cancels the pending start and returns to Ready (quietly, with a log entry — no dialog); a device step already in flight finishes before the cancel takes effect. Preparing is a transactional admission phase. The app revalidates the destination, creates the session's partial artifact, initializes the muxer and encoder, and opens every selected source before it transitions to Recording. A failure rolls back opened resources and reports the actionable cause. A failed startup or precondition before an explicit start is `Blocked`; a failure after the user's explicit start is `Failed` or `Error`, never a false Ready state. **Live monitoring.** While recording, low-cost instrumentation (aggregated off-thread, ~1–4 Hz, no per-frame image analysis) tracks dropped/duplicated frames, A/V drift, and disk-fill ETA, and classifies the pipeline as encoder-, capture-, or disk-bound. Root-cause correlation is surfaced (the first showcase being VRR/refresh-rate vs CFR-capture judder). The six capture-pipeline cards show live status (Healthy / Busy / Bottleneck) with a CPU/GPU tag and one secondary number. A live A/V drift and output-size overlay is available. An on-screen diagnostics overlay exists but is **off by default** (enabled in Advanced). A/V drift is measured, not inferred: the audio device clock (WASAPI per-packet device-position/QPC timestamps) is compared against the QPC timeline video frames are paced on, normalized at capture start and smoothed over roughly one second of packets. Positive values mean audio leads video. Pipeline queue depths and encoder output timing play no part in the number. A track that merges several audio sources mixes multiple device clocks, so drift reads as unavailable for it rather than showing a guess (a single gain-adjusted source still reports). **Audio clock slaving.** Because the audio device crystal and the video (QPC) clock differ by tens of ppm, a long recording would drift out of sync (50 ppm ≈ 360 ms over 2 h). On by default and codec-independent, clock slaving gently corrects this: the audio output timeline is resampled by a sub-audible amount (≤ 0.05 %, well under the pitch perception threshold), pulling audio back onto the video clock. The A/V drift number then shows the **residual** — what actually remains in the file — with the raw drift and the applied correction visible in diagnostics. It engages on either of two facts: the measured drift has already crossed ~15 ms, or the measured **rate** says it will within ten minutes. The second is what keeps the first ten minutes of a recording in sync rather than watching the error grow to the threshold — at 30 ppm that took eight minutes, and every clip cut out of them carried the offset. The rate is read as the slope of the drift after a minute of measurement, never as drift divided by elapsed time, so a fixed head start between the two clocks is not mistaken for a rate. A device whose rate is too small to matter over that horizon is never engaged and keeps its byte-identical passthrough, which is why most recordings still never trigger it. Once engaged, the correction is the measured rate plus a proportional pull on whatever misalignment is already on the timeline, under a fixed rate cap: the residual closes to zero instead of parking, and a clock error beyond the cap leaves a small bounded residual rather than an audible artifact. The *Audio clock slaving* expert toggle turns it off for byte-exact archival capture. A multi-source merged track is not slaved (it mixes several device clocks). **Post-flight report card.** After each recording, a report card surfaces frame-drop %, peak A/V drift, and overall pipeline health. **Real** frame drops raise no toast of their own; they appear as their own coral mark on the Last session card's timeline (§11), and the same three values ride along into the Edit/Output/Save surface as an icon at the right end of its header whose tooltip carries them. The icon is a quiet info glyph while the pipeline was healthy, and an amber or coral warning triangle with a short label beside it when it was not — so a real finding is not hidden behind a hover. A drop counts as **real** when the recording lost picture it should have had: the encoder could not keep up (backpressure), or a captured frame failed to be processed into an encodable frame. Two other categories are **benign** and never count towards the reported drop figure, the toast, or the pipeline-health readout: *coalescing*, where the source produces frames faster than the recording's frame rate and the surplus is deliberately discarded (a 144 Hz display recorded at 60 fps), and *CFR pacing*, where a scheduled frame slot passes before the first frame has been captured at all. Every surface that reports drops — the report card, the "Review" panel, the live counter, the toast, and the Diagnostics capture card — uses this same definition, so they can never disagree about whether a recording dropped frames. Diagnostics additionally breaks the total down by category. **Reported duration.** Everywhere a *finished* recording's length is shown — the completed-state timer and result card on the Record page, the recording-history entries, and the length of the editor's trim timeline — the number is the **media duration of the file**, i.e. what a player reports. Paused time and the stop/finalize tail are not part of the media and are therefore not counted, so a 60 s recording paused for 30 s reads as 1:00, not 1:35. For a split recording the reported total is the sum of the segments' media durations. The **live timer during recording** is a separate readout with its own semantics: it counts the running session and keeps advancing while paused, so the user can see how long ago they started. **Automatic split (time + size).** Two independent auto-split axes — **maximum duration** and **maximum file size** — with "whichever comes first" behavior. Size is reported honestly as "approximately N GB" and measured from committed container bytes (no file-size polling); split boundaries stay keyframe-safe; counters reset per segment. Split is supported for MKV, WebM, and MP4. For MP4, each completed segment is remuxed to progressive MP4 in the background while recording continues; "Saved" is reported only once all segment remuxes finish. Manual split is independent of automatic split. The split controls are Default tier — always visible, laid out inline within the Output card (time and size sub-sections), not tucked behind a popover or an Expert-mode gate. Each sub-section leads with an on/off **toggle**; the interval selector (**Split by time**) and the segment-size field (**Split by size**, in MiB) appear only while their toggle is on. Toggling off is exactly the "off" state — it changes no persisted value beyond the split mode itself, so presets and exported TOML round-trip identically. **Output destination and live artifacts.** The destination is presented as one control: a chip carrying a folder mark, the path, and a trailing ellipsis, which opens the native folder dialog. It replaced a path field with a Browse button beside it — two controls for one value, where the field took the slot's width, still elided a real folder away, and was almost never typed into. The path is elided from the LEFT, because the tail is what identifies a folder. A consequence stated plainly: a path can no longer be typed or pasted, only chosen. The default recording destination is the Windows Known Folder `FOLDERID_Videos` plus `ExoSnap`, respecting folder redirection. Live artifacts are created on the configured output volume, never in the system temporary directory. The valuable unfinished file uses the final base name plus the final extension and `.partial`, for example `Recording.mkv.partial`; disposable staging uses `.tmp`. A clean MKV/WebM session is atomically renamed from its `.partial` name to the final name. Crash repair may use a separate `.tmp` artifact, but if repair fails the original `.partial` is preserved. The configured path is preserved as entered and supports redirected folders, symbolic links, junctions, mapped drives and UNC paths. Validation is asynchronous for network destinations and runs at startup, after path edits, on application activation, after relevant network changes, whenever Settings > Output opens, and during final start preparation. Missing, unreachable or unwritable destinations are `Blocked`/error conditions: Record remains the stable disabled CTA, a startup notification opens Settings > Output, the destination row receives focus and shows the inline error. There is no permanent `Check again` action because these lifecycle events recheck it. **Low-disk guard.** A configurable soft **warning threshold** (default around 2 GB free) shows a Diagnostics notice but still allows recording; a lower **hard-stop threshold** (default around 500 MB free) blocks recording at start and stops a running recording gracefully. For MP4 sessions the effective hard-stop threshold is raised to account for the transient MKV and output MP4 coexisting during remux (roughly 2× file size), and for split MP4 sessions it is raised further by the sum of pending background remux jobs plus the live segment estimate. When destination admission has proven that the path is reachable and writable but its volume's free space **cannot be queried**, the guard cannot measure and therefore does not fire: recording proceeds and a warning is written to the log stating that low-disk protection is inactive for that session. An unreachable or denied destination fails admission instead. A volume that reports **zero bytes free** is a full disk, not a failed query, and is blocked like any other hard-stop. **Filesystem checks.** ExoSnap detects the output volume's filesystem. A **FAT32** volume raises a Diagnostics **notice** about the 4 GiB per-file limit; recording is **not** blocked and short clips work correctly. NTFS, exFAT, and others pass silently. There is no automatic split at the 4 GiB limit. **Crash recovery (Finish / Continue / Delete).** A recovery manifest is written before each session. If a session is interrupted, the next launch shows a recovery overlay offering, per interrupted recording: - **Finish** — saves the recording as originally configured (MKV repair/rename or MP4 remux, honoring the manifest snapshot; no user format choice). - **Continue** — shown only for non-finalized (true-crash) artefacts. Arms the coordinator paused; Resume starts the next recording slice aligned with the per-segment machinery. A 1–2 s data loss at the crash boundary is accepted and visible as the slice boundary. At most one Continue session can be armed at a time; choosing Continue on a second candidate finalizes the first. - **Delete** — an inline two-step confirm that permanently removes the artefact. It is visually set apart from the safe Finish/Continue actions (right-aligned, destructive tint) to avoid mis-clicks. - **Decide later** — an explicit text button; entries stay in the manifest and the overlay re-shows at the next launch. Continued sessions produce independent recording slices (no single-file concat). For MKV/WebM split recordings, segments finalized before an interruption remain usable; an interrupted active segment may not be recoverable. **When the manifest cannot be written.** The manifest is a safety net, not part of the recording, so a failed write never blocks a start or aborts a running recording. It is also never hidden: ExoSnap says so with a **"Recovery protection unavailable"** notification (amber — the recording is fine, only its crash-recovery entry is missing) and treats the session as unprotected from then on, rather than carrying on as if an entry existed. A recording made while the manifest is unwritable simply cannot be offered for recovery afterwards. Only entries with something to recover are offered. An artefact that is **missing** and one that is **empty** are both dropped from the manifest at the next scan — an interruption before the muxer wrote its first byte leaves a zero-byte file, and offering it would promise a recovery that cannot happen, at every launch, until the user deletes it by hand. **Edit / Output / Save (post-stop surface).** A single view lets the user trim and export without leaving the app: the player and its trim timeline on the left, and a right rail with two cards — **Details** (duration / size / resolution / frame rate / video / audio / container as right-aligned mono values) and **Export** (the output choices plus the progress and result of a run, described below). One action — **Export** — sits **bottom-right**, matching the Record page's transport actions; it starts the export straight away and is unavailable while one is running. When an export is possible, Export is the **accent-filled primary action**: it is the one thing this surface exists to do, and an outlined control there was indistinguishable from a dismiss button. It also takes the **pill shape** the Record page gives its own primary control, so a surface's one defining action is recognisable by shape and not only by fill; ordinary buttons elsewhere keep the standard rung. **The player's transport toggle is drawn on demand, not permanently.** The picture is what the surface is for, so the central play/pause control stands while the preview is **paused**, while the pointer or the keyboard is on it, and for a moment after a transport change so the new state is legible; it fades out while the clip **plays** and while the playhead is being **dragged**, which is exactly when an element over the picture hides the frame the user is looking for. Fading never removes it from the keyboard: it stays a tab stop and stays operable throughout (§10). **It is a workspace, not a dialog.** Ownership is unchanged — it is still a layer over the Record page rather than a navigation destination (ADR 0022), and Record remains its parent context — but it **occupies the normal page content region**, below the shell's own title band. It carries no scrim, no floating gap, no outer rounded frame and no outer border; its shape comes from the header divider, the panel boundaries inside it and the footer divider. The previous presentation dimmed the whole application behind a rounded rectangle nearly the size of the window, which read as a modal question the application was waiting on an answer to — and covered the shell's own minimize, maximize and close buttons, so a window with the editor open could not be closed or dragged by its own chrome. While the workspace is open Record stays marked as the current destination, because the workspace belongs to it. The navigation tabs stay **available**: leaving Record ends the temporary edit session without confirmation, exactly like Back or Escape, and returning shows the normal Completed state (§2). Back is a navigation action drawn as one, with the shared chevron. **Header.** One line: `‹ Back`, the title, the clip's file name, and the post-flight **report status** at the right end. The file name is the only element allowed to give up room and elides in the middle, so neither Back nor the report status can be pushed off the band at the minimum window. The report is a **status, never an action**: it names the pipeline's verdict (`Good`, `Warning`, `Critical`, `Unavailable`) in a severity-toned badge behind a fixed `Report` label. It used to be a single badge that read `Report` when nothing was wrong and `Warning` when something was — one element switching between an action and a verdict, with a tooltip as its only behaviour either way. **Structure.** Three bounded areas inside the workspace — player, timeline, inspector rail — each with exactly one boundary. The timeline sits in its own panel: its track already had a hairline, but the label zone, the loading hint and the clock row sat outside it on the bare page background, so next to two bordered panels the timeline read as loose furniture. Nested elements do not repeat their container's border. A successful recording normally remains in the Completed state with **Edit** as the recommended action. The **Open editor when finished** toggle (Settings > Output, off by default) may open this workspace over Record, preloaded with the new clip; it never navigates to a sixth top-level page. With the toggle off, a `Recording saved` toast offers **Edit** and, depending on whether the session ledger observed a problem, either **Show in folder** or **View diagnostics** (§11). The surface scales with the window. The right rail narrows as the window gets tighter and scrolls when its two cards need more height than the window offers, but it is never hidden — it carries the export controls, and the surface stays fully usable down to the minimum window size. At the narrowest rail the **Details** card tightens — smaller card padding, shorter fact rows, and rule lines only between fact groups rather than under every row — which hands roughly 50 px to the **Export** card below it. No fact is dropped and no value gets smaller; only the space between them. A wider window keeps the roomier card. The export never covers the clip it was started from: the only thing that ever interrupts the view is the overwrite confirmation. **Nothing in the rail moves when an export changes state.** The rail's scroll position is the user's; starting, finishing or failing a run never scrolls it, and the tops of the Details and Export cards stay exactly where they were. Trimming is **direct manipulation on the timeline** under the player — there is no button row or duration readout above the strip: - **Rows.** The timeline is a stack: one video row, then one row per audio track the recording carries. Audio rows show the track's name as muxed (`System`, `Microphone`, `Application`, or a combined name for a merged track); a recording written before track names were muxed falls back to `Audio 1`, `Audio 2` rather than guessing a source from the track order. A recording with no audio has no audio rows. Audio rows carry a **label and a fill only — never a waveform**, since a peak envelope means decoding the whole soundtrack and an approximated one would state something about the audio that was never measured. Handles, markers and the playhead span every row: a trim applies to the whole clip. - **Thumbnails.** The video row shows frames decoded from the recording, each drawn **left-aligned at its own timestamp** so a tile answers "from here on it looks like this". Tile width follows the recording's aspect ratio and the number of tiles follows the available width, so resizing the window re-lays the strip. While tiles are still arriving, a compact indeterminate progress indicator and **`Generating timeline previews...`** sit in the quiet zone above the row stack and disappear once every tile the current width can hold has landed. Tiles appear as they finish decoding; a position whose frame is not (yet) available leaves the row empty there rather than showing a placeholder. When the clip carries **nothing decodable at all** — it cannot be opened, it has no video stream, or a decode pass finished having produced no frame - a static broken-preview glyph and **`Timeline previews unavailable`** replace the progress treatment and stay there. That is a terminal statement: no tile is coming. The strip is the only thing affected - trim, playback, markers and export all keep working on a clip whose thumbnails cannot be produced. Cancelling a run (resizing the window, switching clips, closing Edit) is never treated as a failure. - **Trim handles.** Draggable in/out handles sit at the start and end of the timeline; the trimmed-away ranges are dimmed. The handles constrain each other (they can never cross), and on release the cut point snaps to the nearest keyframe at or before the requested time and, within 50 ms, to the nearest marker — trim stays **keyframe-accurate and lossless** (stream copy, no re-encode). The trim is applied only when the user clicks **Export**; until then nothing on disk changes. The Export card states `Changes are applied only when exported.` - **Playhead.** A white playhead line moves through the timeline while the preview is playing (play/pause toggle on the player area). **Scrubbing** — dragging the playhead or pressing anywhere on the track — jumps the preview position there; playback pauses for the duration of the drag and resumes on release **only if it was playing before** (paused stays paused). - **Drag feedback.** While a handle or the playhead is being dragged, it scales slightly and a centered time label appears above it: `MM:SS.mmm`, with an hour field (`HH:MM:SS.mmm`) only when the recording is one hour or longer. - **Markers.** Markers placed during recording render as thin secondary-accent (violet) verticals across the timeline, positioned proportionally (a recording with unknown duration shows an inert timeline rather than guessing). The caution color is deliberately not used here — it stays reserved for a real diagnostic warning elsewhere in the app, and Studio Mint stays reserved for the active trim handles/playhead. Marker editing is not part of the edit surface. Markers are **never written as container chapters**. Instead, a trimmed/remuxed export writes a JSON **marker sidecar** (`.markers.json`, ADR 0042) next to the exported file — but only when markers survive the trim. Surviving markers are re-based to the trimmed clip's start; markers cut away by the trim are dropped, and an export with no surviving markers removes any stale sidecar at the destination instead of writing an empty one. The **Export** card in the rail offers container **MKV / MP4** (both stream-copy, lossless) and a save mode of new file (`_edit.`, saved beside the source) or overwrite-original (atomic rename in place); two short lines under them state what the current choice writes and where — `Lossless stream copy` followed by either `Saved beside the source as "_edit.mkv"` or `Replaces the original recording`. The save mode alone determines the destination — there is no separate destination-folder picker, since the model leaves nothing else for the user to choose. Overwrite-original asks for confirmation before the export starts, naming the file it will replace; the destructive choice is not the default button. The same card reports the run, and it reports it **directly under the card's heading, above the output choices** — so a result is visible without scrolling and without anything above it shifting. There is a progress bar with **Cancel** while it is going and the result with **Show in folder** when it succeeds. A failure has exactly one retry route: the primary Export action remains unavailable while the failure result is present. If the destination is missing, unwritable or full, the result offers **Choose another folder** through the native Windows folder picker and retries once to that one-time destination. It may also offer **Open output folder** when useful. A raw Retry that cannot change the cause and parallel Retry/Export actions are not shown. A successful run names the file on **its own line and the folder on a second one**, each elided in the middle, with the full path on hover. The two were one wrap-anywhere run, which in the narrow rail broke lines inside the extension (`…2026-08-10 2` / `1-14-08_edit.mkv`) — the one part of a path a reader scans for. There is **one** folder action, not two: `Show in folder` opens the containing folder *and* selects the file, so it does everything a separate `Open folder` did, and two labels for one outcome is a choice the user cannot win. The follow-up actions stay stacked (the rail is too narrow for them side by side) but are kept compact, so the finished state is not conspicuously taller than the other three. In the resting state the card reserves no space for the status it is not showing. The output choices stay visible throughout — greyed out while a run is in flight — so a finished export leaves the settings in place for the next one; at the minimum window size they are what scrolls out of view. A **keyframe interval** selector (Settings > Recording format: 2 s default / 1 s / 0.5 s) trades a little file size for finer trim accuracy. The original recording is never mutated during export. Trim points, markers and future edit operations are a temporary in-memory export recipe and are never written as drafts or project state. Back, Escape, tab navigation or opening another clip discards an unexported recipe immediately, without a dirty badge, prompt or recovery entry. A running export owns an immutable snapshot of the recipe and continues after the workspace closes; completion or failure is reported through notifications. Dismissing the surface closes the clip; the preview decoder and the thumbnail strip's decoder are released, and the recording can be moved, renamed or deleted again without quitting ExoSnap. Opening Edit afterwards starts from a clean session. **Current boundary:** trim, markers, stream-copy export, and real decoded-frame preview (video + synchronized audio, FFmpeg-decode + Qt-paint) are implemented and reachable end to end, including the playhead/scrub/trim-handle interactions against the real decoder. This covers 4:2:0 recordings (8- and 10-bit) as well as a 4:4:4-chroma recording (Section 6's Expert 4:4:4 option, H.264/HEVC 8-bit only) — the player's decoder converts the fully-planar 4:4:4 pixel format the same software decoders produce for that option, with no chroma-upsampling step since 4:4:4 carries full color resolution per pixel. A recording made in native HDR10 (PQ / BT.2020, 10-bit) is tone-mapped to SDR for the preview through the same reference curve the recording preview uses, so the same frame does not change colour between the two views; the tone-map is applied only for unambiguously PQ-tagged 10-bit material, and anything else takes the SDR path. The tone-map targets the reference display peak rather than the peak of the screen the editor happens to be on (see `KNOWN_LIMITATIONS.md`). Export is unaffected by any of this, since export is pure stream-copy and never depends on the preview decoder. The Split Chapter action remains deferred to a later release (0.11 per ADR 0022). --- ## 9. Presence and notifications - **Tray icon** with **idle / recording / processing / paused / saved / failed** states and an **unread notification badge**. Saved is green, is shown for the same dwell as the "Saved" notification, and then returns to idle on its own; a starting recording takes the icon back immediately, whether or not that dwell has run out. An icon that stayed green would have stopped describing the application and started describing history. **Processing** is the mark for work the user cannot interrupt — the save or remux after a stop — and appears only once that work has lasted long enough to be worth reporting; a stream-copy save is usually over before it does. **Failed** is coral with a cross and stays until the next recording, because a failure nobody has seen yet is what a shell surface is for. - **The tray icon carries the chosen accent.** The mark's outer ring is whichever accent the Appearance settings are on and follows a change immediately, with no restart; the inner ring and what is inside it is the session's state colour, so the accent is never a second state channel. At the sizes Windows draws a notification-area icon the mark is drawn with heavier strokes and less margin than the full-size logo — the same drawing, corrected so it survives 16 pixels. At 16 pixels the colour is what carries the state: there is not room inside the aperture for a pause bar or a spinner to read, and the glyph starts to tell from 20 pixels up. - **The mark beats for as long as the recording runs.** The tray icon and the taskbar button pulse on a second-and-a-half loop while a capture is live: the light travels outwards from the centre dot to the inner ring and back, and rests at the bottom of the loop before starting again. Only the BRIGHTNESS moves. Nothing in the mark changes size, which is what makes a permanent beat readable at 16 pixels -- a beat that moved the rings differed from frame to frame by less than a screen pixel there and read as a flicker rather than as a heartbeat. **Pausing stops the beat on the spot**, and resuming starts it again from the bottom of the loop. A countdown shows the recording state but holds it still, which separates "committed" from "capturing" at a glance; paused, saved, failed and idle are static. **Processing also animates for as long as it lasts**, because what bounds it is the operation itself and the taskbar's progress bar is running beside it. **Inside the application the recording indicator breathes on the same loop** -- the on-screen pill swings further, because a ten-pixel dot over a full-screen game has to carry further than a tray icon does. - **The taskbar button shows the same mark as the tray.** One image, two surfaces: while a recording runs the button's icon is the coral mark and beats with it, paused it is amber, saved it is green, and failed is a coral cross. What Explorer, the desktop and Start show is a different thing and does not change — that is the icon of the *file*, in the shipped accent, whatever the application is doing. - **Tray menu** — in order: a **blocked-reason row** when it applies, the transport (**Start recording**, **Pause**/**Resume recording**, **Stop recording**), **Show window**, **Open output folder**, **Notifications**, and **Quit ExoSnap**, with a separator after the blocked-reason row when it is shown, after the transport and before Quit. Every entry that acts carries a glyph. The menu is a **native Windows popup menu**, not a surface the product paints itself, and it follows the **Windows apps-colour setting** rather than the application's own appearance — the same dark or light ground every other context menu on the desktop uses. The tray **icon** follows the taskbar's own setting too, for the same reason (both are composited onto a ground the product does not own), so the icon and the menu popping out of it can legitimately differ from the application's chosen appearance, and from each other if Windows and the taskbar disagree. The desktop notification toast follows the same shell setting as the tray, for the same reason: both are composited next to Windows' own chrome, not over anything the application itself owns or is recording. There is deliberately **no status caption** naming the session at the top of the menu: a native popup menu has no header item, so a disabled first row would read as a broken command rather than as a caption, and the state is already carried by the icon itself (idle, recording, paused, saved, processing, error, with the recording beat) and by its tooltip, which names the session in words. **The menu's shape otherwise does not change.** Every transport row is always there; a row the state does not allow is **greyed under its own name** rather than removed. A menu is read as the list of what the application can do, and an entry that is present in one state and absent in the next teaches nothing about why — a control that vanishes reads as a bug, a greyed one reads as a reason. It also means the pointer lands on the same row twice in a row. The **blocked-reason row** is the one row that comes and goes: it appears only while a start is actually refused and only when the reason is known. It reads as a sentence, *Cannot record: \*, and carries the caution glyph rather than an empty icon gutter, so a disabled row reads as information rather than as a dead command. The transport is enabled by exactly what the state allows: Start while idle, Pause and Stop while recording, Resume and Stop while paused; Start stays greyed rather than gone while a recording is being prepared or saved. **Show window** sits below the transport, is always offered and always brings the window to the front — from the tray, from behind another window, or from a minimized taskbar button. There is still **no Hide entry**: hiding is already carried by the window's own controls, and a menu row for it was a second name for a gesture the user has in front of them. **Open output folder** opens the configured recording destination in Explorer and is offered in every state; if that folder does not exist, nothing is opened and nothing is created — a missing destination is a settings problem, and Settings is where it is reported. **Notifications** is always offered — it opens the window and clears the unread count, which is never the wrong thing to do — and carries the count in its label only while there is one. - **Clicking or double-clicking the tray icon** brings the window back; **right-clicking it** opens the menu. Recording is toggled by the global hotkey and by the menu's transport rows, never by a click on the icon — a double click is too easy to produce while reaching for the window. - **Taskbar button** — the same transport as a thumbnail toolbar under the taskbar preview (Record, Pause/Resume, Stop, following the same table as the tray menu), a fourth button that **opens the output folder** and is offered in every state — the strip is registered once and cannot change, and opening the destination is safe whatever the session is doing — the state in the button's own icon as described above, and a **progress bar** for the long operations: the save after a recording stops, a recovery finish, and an edit export. The bar shows one operation at a time; a second long operation running at the same time reports in its own surface rather than sharing the bar, and a failed operation leaves the bar red until the next one starts. Taskbar integration is a convenience and never a dependency: if Windows refuses any part of it, the buttons and badge are simply absent and recording is unaffected. The strip's ground is drawn by Windows and follows the **system** appearance, so its glyphs are drawn in the state colours of that appearance rather than the application's: the dark palette lightens its state colours for a dark ground, and using them on a light strip left the amber pause mark short of the contrast the palette is measured for. A system appearance change mid-session re-pushes the buttons with glyphs resolved anew, because the strip cannot be registered twice. This is the one place where a product surface follows Windows instead of the chosen appearance, and it does so because the surface underneath it is Windows'. - **Notification hub** — the canonical notification center is a **bell icon with a notification hub panel in the app header**. **The hub is the record: every notification lands there**, persists until dismissed, and keeps its action (recover, undo, show in folder, …). The **system-tray icon additionally shows an unread badge** for the same items. The "Show notifications" setting gates only the toasts — the hub records regardless. **A hub entry carries its severity as a glyph and a word, not only as a colour**: the same four severity glyphs the readiness tiles, the issue cards and the inline notices use, and an accessible name that starts with the severity ("Warning. Storage running low."). Colour alone would say nothing to a user who cannot separate those hues and nothing at all to a screen reader. The empty hub is content-sized around its empty-state message instead of retaining the filled panel's height. Entry actions use a compact text-button treatment with defined action colour and a small chevron; they never read as unmarked body copy and never become large primary buttons. - **The bell's unread dot** carries urgency, not a count. It appears whenever anything is unread and takes its colour from the **worst** unread entry — **mint** when nothing unread is more than a notice, **amber** when at least one is a warning (frames dropped, a source degraded, a dead hotkey, a repaired settings file, a settings file that could not be read, an omitted overlay, a recoverable session, a recording without crash-recovery protection), **coral** when at least one is a failure (unexpected stop, low storage stopping a recording, a failed settings write, a rejected capture action). The exact number is deliberately not shown in the title bar: it is never the thing you act on, and the hub states it in full one click away. - **Toast notifications** — a transient glance at the hub, anchored bottom-right **of the screen hosting the ExoSnap window**. A notification is **timed** when it reports an **event that already happened** and **standing** when it reports a **condition that is true right now and will clear itself** when it stops being true. Only three conditions qualify: low storage, an audio source that lost its device, and a window capture that stopped producing frames — each is dismissed the moment its condition ends. Everything else is an event and leaves on its own, including a recording that stopped unexpectedly and an unfinalized recording offered for recovery: neither will ever be cleared by anything, so standing meant *forever*, and neither loses anything by going — the hub keeps every entry and the recovery surface offers itself again at startup. Timed toasts have exactly two dwells, chosen by whether there is anything to do about them. **10 s** when the card offers a way to act or reports a problem worth noticing — long enough to read it, decide and reach the button, including while the user is still coming back from whatever was being recorded. **5 s** when a glance is the whole interaction (a repaired setting, an omitted overlay). Nothing is longer than 10 s: past that a toast reads as standing, and the reflex to dismiss toasts unread is what would cost the three real standing notices their effect. At most one timed toast is visible — a newer one replaces it; standing toasts stack above it, never auto-dismiss, and always carry an explicit action out. A countdown bar appears exactly on the toasts that leave on their own. The card grows to fit its content: no reserved space for an absent body; the body word-wraps and the card grows with it up to three lines, ellipsizing beyond that (the hub always keeps the full untruncated text); with a single action the card itself is clickable; two actions get named buttons. A preset switch raises no toast — only the hub entry with **Undo** (the combo box that switched is the way back). - A toast is **operable**: its dismiss ✕ (present on hover) and every action it offers respond to the mouse or keyboard. The toast window never takes keyboard focus — taking it would pull focus out of the game or editor being recorded — so no card is ever the focused one. **Escape** therefore acts only while an ExoSnap window is the focused window, and it dismisses the toast nearest the anchor: the timed toast when one is showing, otherwise the newest standing one. Only the transparent gaps between stacked cards fall through to whatever is behind them. - A body that has no place to wrap — a file path is one unbreakable token — breaks mid-token rather than growing past the card. No body may overrun the card at any length. - The "Recording saved" toast names the **file**, not its full path. The path is what the actions act on (**Show in folder** opens it, **Edit** receives it), not what the card spends its width on: the user chose the output folder and already knows it. - The toast resolves its colours from the **Windows apps-colour setting**, independently of the application's own chosen appearance — the accent stays the product's own selection. It is the fifth capture-excluded window and the only one of the five that follows the shell rather than staying fixed-dark, because it sits on the desktop beside Windows' own notifications rather than over the recorded picture. - **On-screen overlays**: a recording-status pill (anchored top-right of the recorded monitor), a diagnostics readout pill directly beneath it (**off by default**), a countdown overlay centred on the recorded monitor, and an **opt-in** interactive quick-control pill (off by default). All four are capture-excluded, as is the toast window above them — five capture-excluded windows in total. The first three are click-through; the quick-control pill (ADR 0016) and the toast stack are interactive by design and take mouse input. The other three sit over the recorded picture, which includes the taskbar, so they use the monitor's full rectangle; the quick-control pill instead stays inside the monitor's **work area**, above the taskbar, by default and while being dragged, because it is the one overlay that accepts clicks, so it is the one overlay the taskbar must never cover. It also keeps a **margin off every work-area edge** rather than sitting flush against one, by default and at the end of a drag alike: it is a control surface the user parks among real windows, and one pressed against the edge reads as clipped rather than as placed. **Dragging the grip moves the pill with the pointer, one pixel for one pixel**, until an edge stops it. - The **recording pill** carries a state glyph plus the configured text. There is no REC or PAUSED word — the pill only appears while a capture is live, so the glyph carries the state: a coral dot while recording, an amber pause symbol while held, an amber warning symbol once **measured** frame drops occur. A failed recording does **not** raise the pill: the in-window error surface owns that, and a click-through pill could not be dismissed. - The **countdown overlay** shows whole remaining seconds as a digit, and its ring depletes **continuously**. The ring is a progress indicator, not a second way of printing the digit: a ring that only moves once a second is correct for an instant and stale for the rest of it. - **Overlay content is configurable** in Settings → Overlays, by preset with per-element overrides. Recording: **Minimal** (elapsed time) or **Custom** (elapsed time · file size · source name). Diagnostics: **Health** (only the tokens that can report a problem — drop, drift, muted sources), **Technical** (all of fps · drop · drift · size · muted sources), or **Custom**. Toggling any single element turns the preset into Custom, starting from the set currently shown. - Every offered element has a measured runtime producer; an element with no producer is not offered. A configured-but-unmeasured value renders as an em dash, never as zero. An overlay whose elements have all been unticked does not appear rather than appearing empty. - Overlay **appearance is not configurable**. Opacity, radius, shadow, colour and placement are design-system values, not preferences; the Settings section configures behaviour and content only. Click-through is likewise not a setting — it is a correctness property of a window that sits over whatever the user is recording. - **Minimize-to-tray** is opt-in and **off by default**. With it on, minimizing hides the window instead of sending it to the taskbar, and the tray icon is the way back. With no system tray available the window minimizes normally however the preference is set: hiding a window whose only restore path does not exist would strand a running process behind nothing to click. - Every minimize route obeys the preference — the title bar's own Minimize button, the window menu, Win+Down, and a click on the taskbar button of the active window. **Win+D / "show desktop" is deliberately excluded**: it is a shell-wide toggle whose windows come back with the same gesture, and a window hidden to the tray would not. - **Hiding the ExoSnap window from screen capture** is opt-in and **off by default**. - While on, the window carries `WDA_EXCLUDEFROMCAPTURE` **at all times**, not only during a recording. That is what makes the label true whenever the switch is: a scoped version would also leave the window excluded after any failure path between start and stop, which is the unscoped behaviour arrived at by accident. - The exclusion is a property of the window, so it reaches **every** capture on the machine — ExoSnap's own recording, but equally conferencing, screen sharing, remote support and screenshots. That reach is why the default is off: the failure mode of off explains itself, and the failure mode of on is silent invisibility in someone else's call. - If the platform call is refused, the window stays **visible** and usable and the refusal is logged. This is the opposite of the five overlays' rule, and deliberately so: an overlay in a recording is a corrupt recording, while a main window that disappears because a Win32 call failed is worse than a main window that appears in a capture. - The setting does not reach the five capture-excluded overlays. They are excluded unconditionally, in both positions of this switch. **Closing the window.** The close button always closes. No preference turns a close into a hide, and there is no one-time "still running in the tray" notice, because there is no longer a state it would explain. The only inputs are what is in flight: | Recording, export or remux in flight | Outcome | |---|---| | no | the application closes completely | | yes | the window comes to the front and asks; confirming closes completely | A finalize still in flight is the one case that refuses a close without asking — the container is being written, the finalizing overlay is already on screen saying so, and there is no alternative route that would end the process anyway. Close-to-tray used to sit between those rows, and its removal is the point: a close button that does not close is the surprising half of the pattern. With it on, "close" during a recording silently meant "hide", and the user was left believing a recording had ended when it had not. Minimize-to-tray keeps the useful half — a gesture that already means "put this away" — and leaves close meaning close. **An approved close ends the process.** It does so explicitly rather than relying on the toolkit's "quit when the last window closes" behaviour, because the five capture-excluded overlays are top-level windows in their own right: a standing notification kept the application alive after its only window was gone, with the tray icon still showing and its Quit routing through a window that no longer existed. **Quit** in the tray menu works whether the window is visible, hidden, or already gone. --- ## 10. Hotkeys Global hotkeys are rebindable, with conflict detection and rollback on an invalid bind. They live as an embedded card inside Settings. Five actions ship bound, all on **Alt+Shift**: | Action | Default | Mnemonic | |---|---|---| | Start / stop recording | `Alt+Shift+R` | Record | | Pause / resume | `Alt+Shift+P` | Pause | | Capture a single frame | `Alt+Shift+S` | Snapshot | | Add a marker | `Alt+Shift+M` | Marker | | Split the recording | `Alt+Shift+C` | Cut | The modifier pair is chosen by exclusion as much as by preference. The Alt+F-key row belongs to NVIDIA (F1, F2, F3, F8, F9, F10, F12) and is shared with media keys behind an Fn toggle on many keyboards, with Alt+F4 sitting in the middle of it. **Ctrl+Alt is AltGr**: on a German layout Ctrl+Alt+Q types @ and Ctrl+Alt+E types the euro sign, which rules it out for a product that ships a German localization. Ctrl+Shift is where AMD keeps E, S, I, O and M. Win+anything loses every future dispute with Windows. Alt+Shift is untouched by NVIDIA, AMD, the Xbox Game Bar, Steam, OBS and Discord; its input-language switch fires only when the pair is released with no other key in between, which a letter suppresses, and the crowded space in games is a modifier plus a DIGIT rather than plus a letter. Letters are mnemonic first and one-handed second. A shortcut that is remembered beats one that is merely comfortable, and the two that sit further from the modifiers (Pause, Marker) are also the two nobody presses under time pressure. A default is still only a starting point: it is global, so it takes the combination from every other application while ExoSnap runs, and any of the five can be rebound or cleared. If a hotkey starts recording while the app window is visible, the Record view is activated; if the window is minimized, it is not restored. If a persisted hotkey can no longer be registered at startup (another app or Windows already holds it), ExoSnap **drops the unregisterable binding** (it stays cleared across launches rather than silently swallowing the key or re-warning every start). Windows exposes no way to name the holding process or to reclaim the combo. A binding that cannot be registered is treated by where it came from. One the user chose themselves worked when they chose it, so losing it is worth telling them about. One still at its shipped default is removed **silently**, with a log line and no notification: the user never asked for that combination, and a machine where NVIDIA or another tool already holds it would otherwise greet every launch with a warning about a shortcut its owner never picked. Losing a user-chosen binding at startup raises a notification naming the affected action, with a **Rebind** action that deep-links to Settings → Hotkeys, where the user can bind a working shortcut; attempting a combo already held elsewhere is reported inline there as a conflict. ### 10.1 In-window keyboard operation Global hotkeys are one of four kinds of key handling, and only the first works while ExoSnap is not the focused application. The other three are in-window and are **not** rebindable: | Kind | Scope | Examples | |---|---|---| | Global hotkeys | the whole desktop | start/stop, pause/resume, capture frame, marker | | Window shortcuts | the ExoSnap window | `Ctrl+1`…`Ctrl+5` select Record / Settings / Diagnostics / Logs / About | | Surface-local keys | one page or overlay while it holds the keyboard focus | the Edit timeline's transport keys, the webcam overlay's arrows, `Escape` on a modal, or on the frontmost desktop notification toast while an ExoSnap window is focused | | Text editing | a focused text field | everything else | Every shortcut ExoSnap adds inside its own window is modifier-qualified, so no shortcut can consume a keystroke meant for a text field. The five navigation shortcuts are inactive while a blocking surface (recovery, crash report, recording error, release notes) or an unanswered close prompt is open, for the same reason the navigation tabs are: nothing may swap the page under a surface that still covers it. The close prompt is named separately because it is the one case a scrim cannot cover on its own — the desktop notification toast is its own always-on-top window and reaches the same navigation intents from outside the shell. An open **edit workspace** is not one of those: the shortcuts and tabs share one navigation contract, and either closes the temporary edit recipe without confirmation before navigating (§2). **The pointer says what a control does.** Anything that can be clicked shows the **pointing hand**; a control that is present but **unavailable** shows the plain arrow, because a greyed control that still offers the hand is inviting a press that will not happen. A text field shows the **I-beam**, a draggable element the **open hand** and, while it is being dragged, the **closed hand**, and a frame that can be moved or resized the matching **move / resize** cursor. **Every interactive control is reachable and operable with the keyboard alone.** A control that is semantically a button, a toggle or a selectable item appears in the tab order, shows a focus ring while the keyboard put it there (never after a click), and is activated by **Space** — the Windows convention, and Qt's own; Enter belongs to a dialog's default button and ExoSnap adds no second activation key of its own. A group of mutually exclusive choices (a segmented control) is one tab stop, with the arrow keys, Home and End moving inside it. The **Edit surface** is fully operable without a pointer. The trim timeline is a single tab stop: | Key | Effect | |---|---| | `←` / `→` | move by one second, `Shift` ten seconds, `Ctrl` a tenth of a second | | `Home` / `End` | jump to the clip's start / end | | `[` / `]` | choose whether the arrows move the playhead, the in point or the out point | | `I` / `O` | set the in / out point at the playhead | | `Space` | play / pause | --- ## 11. Diagnostics and fix actions Diagnostics is a first-class engine, not a status readout. Its posture is **calm, not alarmist**: it defaults quiet, reports only real/measured problems, gives one primary fix per problem, hides depth behind collapsed reference rows, and always shows hard blockers. **Measured cards added for game recording (v0.9).** Each has a source, a meaning, a cadence, a UI consumer and a log consumer: - `rec.pacing.duplication` -- more than 25 % of the emitted frames over the last five seconds were repeats of the held frame (source below the recording rate). Not raised while the source is starved, which is the stall notice's story. Remedy names the frame rate, not the encoder. - `rec.audio.endpoint_taken` -- a degraded source whose endpoint is still present but refused reactivation as **in use**: another application holds it in exclusive mode. Replaces the generic *"Audio device lost -- reconnect"* card for that cause, whose remedy would send the user to the wrong place. - `rec.audio.clock_saturated` -- the audio clock-slaving controller is at its rate limit and the residual keeps growing; the card states the drift in ms per hour and names the device's clock, not the settings. - The present-cadence cards (`rec.present.discarded`, `rec.present.modeflip`) are raised only for a **window** target. For a display or region the present sample spans every process on the desktop and says nothing about the recorded content. - A frame captured into the phase-correct ring and overwritten before it was emitted is booked as a real drop (`frames_dropped_ring_eviction`), on every surface that reports problem drops. - An encoder open that fails with `NV_ENC_ERR_OUT_OF_MEMORY` is reported as **"Encoder is in use by another application"** with the usual NVENC clients named, never as a driver problem. - `rec.capture.adapter_mismatch` (Blocker, at target selection) -- the captured display is driven by an adapter that is not the NVIDIA GPU while an NVIDIA GPU is present: the encoder opens on the display's adapter and cannot reach it. Names the adapter and the NVIDIA Control Panel setting. - `rec.output.drive_kind` -- the output folder is on a network, removable or optical volume (`GetDriveType`), said before the first write stall rather than after. - `rec.gpu.contention` -- the pipeline's `GPU` bottleneck: the recorder's own passes finish late on the GPU (D3D11 timestamp queries) while submitting them stays cheap; the card is saturated by the captured application. Replaces the compositor label that used to take the blame. - Present cadence for **window and region** capture comes from the WGC frame's `SystemRelativeTime` deltas, so `rec.001` (judder) is reachable there too; its provenance is a delivery time, so the jitter floor is higher than DXGI's present time. For a **display** the present sample is attributed to the foreground process on that display, re-resolved at the diagnostics cadence. - **Default microphone changed** -- a notification when Windows moves the default capture endpoint while a session that followed the default is recording; the session keeps its device, and the notice says so. **Underlying severity:** each check resolves to **Pass / Notice / Blocker**. A **Notice** is advisory and never blocks recording; a **Blocker** prevents recording from starting. **The four-tier honesty model.** Every diagnostic **declares its own tier** as part of the diagnosis — the tier is not re-derived downstream from an id list. The tier sets both the color and the default visibility, and the honesty rail is a hard rule: **hiding is only ever for noise (Tier 3 + 4); a real problem (Tier 1 + 2) is always visible, in every state of the page.** No check may show a Tier-3 optimization in a warn color. - **Tier 1 — Blocker** (coral). Always shown; gates recording start. - **Tier 2 — measured problem** (amber). A live or environment problem that was actually *measured* (judder, disk write-stall, low disk, audio device loss, unresolved saved display). Always shown as its own card. Only ever fires when the condition is real — never predicted. - **Tier 3 — optimization tip** (mint, "better, but it runs" — codec, container, color range, FAT32, Opus-in-MP4). Bundled into one quiet tip chip; never turns the verdict amber. - **Tier 4 — fact** (neutral). Capability/environment facts (elevation baseline, live audio format). Run through the same model, shown only in the **Environment & configuration** reference row, never counted in the verdict. **Entry cards.** Each Tier-1/Tier-2 card carries a mono **ID chip**, an optional **Elev** lock badge (for checks measured from the elevated present-path baseline), its typed FixAction, the measured value and its budget inline, and a summary stating the why. Only a log excerpt stays behind a collapsed **Evidence** disclosure, alongside a **Show in log** link that opens the Logs page filtered to that diagnostic id. The Diagnostics page is state-ordered rather than split into a Simple and an Expert view: the recording state decides what is shown, and there is no Expert control on the page at all — the global Expert toggle stays a Settings-only concern (§12). It is a top-anchored dashboard that fills the page height rather than a centered, compact statement, one layout across three states: - **Idle** — verdict band, four readiness tiles, Tier-1/Tier-2 cards, one bundled Tier-3 tip chip, reference rows. - **Recording / Paused** — verdict band, a six-stage pipeline rail, live tiles, the session ledger, Tier-1 cards, the tip chip, reference rows. Readiness tiles are hidden while recording. - **After Stop** (until the next recording starts) — verdict band back to the readiness verdict, the Last session card, readiness tiles, cards, the tip chip, reference rows. **Reference rows** are the same four collapsed disclosures in every state, each with a one-line summary in its header: **Self-test** (with "Run again"), **Hardware capabilities** (with "Rescan"), **Environment & configuration**, and **Support bundle** (with "Create"). There are no numbered section titles and no "full taxonomy" subtitle. The band carries no Run-check control: the page probes on first visit (with the self-test), every 10 s and on every settings change, and the band's own stamp says so ("Checked 15:54 · rechecks every 10 s and on every settings change"; "Recording since 15:54 · live 5x/s" while a session runs). Tile rows are always full — four columns or two, never a ragged three — reflowing the same way the capture-pipeline rail does. **Readiness tiles.** Four, always in this order: **Encoder**, **Disk**, **Display**, **Audio**. The Encoder tile's codec row is H.264 · HEVC · AV1 in that fixed order, the selected one outlined in accent and a codec this GPU cannot encode dimmed with a cross; the tile itself is coral only when the selected codec cannot be encoded here or no encoder exists at all. The Display tile's sub-line carries the capture target ("144 Hz · Desktop – Display 1"); there is no separate Capture-target tile. The verdict band already states the overall rollup, first and larger, so there is no separate Readiness tile either, and the old Last-session tile is replaced outright by the Last session card below. **Live tiles.** Four while recording — Frame pacing, Encoder, Storage, Audio sync — eight with the in-depth switch on, which adds Present mode, Present health, DPC / ISR latency and GPU time as a second full row. The second row is always all four: a tile whose trace is not reporting shows an em dash and names why in its detail line, the same way every other unmeasured value is shown. The row is absent only when the switch is off. **Session ledger.** While recording, a Tier-2 measured problem that fires on two consecutive measurements (never a single spike, and never the same measurement counted twice) enters the ledger, under an OBSERVED IN THIS SESSION eyebrow. Only a check that measures something DURING the run enters: a condition that is a property of the configuration — free space, the pacing mode, the capture target's own mode — was already true before Record was pressed and belongs to the readiness surface, not to the record of what this recording ran into. An **active** entry is an expanded card: filled amber ground, a "now" badge, live duration, its measured value against budget inline, its summary as the why, and a collapsed **Log excerpt** disclosure with a **Show in log** link. A **quiet** entry (fired before, not firing now) collapses to a single 44 px amber-outline row — glyph, title, count, last seen, worst/budget, a "quiet" badge, the id badge — that expands to the full card on click. An entry never leaves before Stop, and entries stay in first-seen order. **Value tint.** The colour of a number comes only from the check that owns it: green inside budget, amber while the owning check is in the ledger, coral for a blocker, and neutral ink when no check owns the number at all. Colour is never the only cue — the tile keeps its glyph and the accessible name keeps the severity word. **Last session card.** Shown After Stop, replacing the old Last-session tile: header ("Recording saved · N problems observed"), the file name, then four fixed facts — **Frames dropped** (tinted critical above zero, ok at zero), **Achieved fps**, **A/V drift**, and **File** (valid, container, codec, size) — a timeline strip below them (spanning the recording's own elapsed time, one amber mark per ledger occurrence, width = its duration; hover names the problem, time, duration and worst value; click opens Edit at that time, clamped to the end of the file so a mark in the tail after the last encoded frame does not open past it), the frozen ledger worst-first with the first card expanded, and the Show in folder / Open in Edit / View log actions. The timeline marks ledger occurrences only: the engine keeps no timestamped record of individual frame drops, so a drop is not marked on the timeline — it is only counted in the **Frames dropped** fact above. **In-depth diagnostics switch.** A header control, not a reference row, and the **only** control over the present/DPC opt-in. It is **session state**: off at every start, on for this session, never persisted and never offered anywhere in Settings. The traces behind it need an elevated process and an elevated process lasts exactly one session, so a remembered answer would be one the next start could not act on. Disabled while recording. Its sub-text states the gate, and the switch and elevation are two separate conditions — "Off · needs an admin relaunch", "On · not measuring · needs an admin relaunch" while the process is still standard, and "On · elevated · PresentMon + DPC/ISR trace" when the traces are actually running. Turning it on is what adds the second row of live tiles above. Turning it on in a standard process raises a **"Restart as administrator"** toast: *"In-depth diagnostics need an elevated process. Restart ExoSnap as administrator to start the traces."* Once per turn-on, never on a switch that is simply already on. Its action restarts ExoSnap elevated through the ordinary close guards, so a running, preparing or finalizing recording refuses it and the app stays where it is; the elevated successor comes back on the Diagnostics page with the switch on and the traces running. Nothing about the offer is a prompt: the switch is already on, the toast has to be pressed, and declining the UAC prompt behind it brings ExoSnap back without elevation, where the switch reads Off again because it is a switch for this session and nothing else. A misclick on the UAC prompt is therefore recoverable from the switch alone. An already-elevated process raises no toast at all; it starts the traces on the spot. The next start is off again, with no toast and no UAC prompt. **Sparkline tiles.** Frame pacing, Encoder, Storage and Audio sync each carry a 24 px sparkline of the last 60 snapshots (12 s at 5 Hz), coloured by the tile's own value tone, with a dashed budget line where the owning check has a threshold. The detail line carries the whole-session figure — average for a rate, p99 for a latency; after Stop the same figures become the Last session facts. **Encoder tile.** Headline is the GPU name with its vendor prefix trimmed ("GeForce RTX 5070 Ti"); below it the encode backend and, when the driver reports one, its version — "NVENC · driver 581.29", with no badge in the tile head. The driver is written the way its own vendor writes it, not the way Windows does: NVIDIA's WDDM number "32.0.15.8129" is shown as **581.29**, which is what its release notes, its control panel and its download pages say; every other vendor publishes the WDDM string itself and it is shown unchanged, and a driver that reports no version at all is simply not named. The container is not on this tile — it is a muxer fact, and it is stated by the pipeline card and the configuration summary. A codec row shows H.264 · HEVC · AV1 with the selected codec in an accent outline, an available-but-unselected codec in ink, and a codec this GPU cannot encode dimmed with a cross. The per-adapter capability matrix follows below, inside the collapsed **Hardware capabilities** reference row (§2) — health first, technical capability second. Its rows are capabilities and its columns are **H.264**, **HEVC**, and **AV1**, so codec differences can be read vertically. Values such as maximum B-frames live in the cells; raw probe details remain below the matrix. The self-test table remains a separate component-by-component result list because repeated Pass results are meaningful there. **FixAction model.** Each detected issue can carry a typed, executable fix action with a safety class, never applied silently: - **Auto** — a safe, reversible change the app can apply after showing a change summary to preview/confirm (for example, reconcile to the nearest valid codec combination, or Full→Limited color range). Auto is normally config-only, but it also covers a **confirmed capture-target change** (e.g. "Record the monitor instead" for an exclusive-fullscreen window). **Rule:** when an Auto fix changes the recording *scope* or *track structure* (not just encoder settings), the confirm dialog with a `changes_summary` is **mandatory** (never one-click), and the summary must name the consequences. For "Record the monitor instead" the summary states that the whole monitor is recorded (other windows, notifications) and that the per-application (APP) audio row is removed (only System/Microphone remain). - **Assisted** — opens the right pre-focused Settings panel or folder, or copies a command (for example, "switch the game to borderless" for exclusive-fullscreen black capture). Assisted cannot execute anything; when the app *can* execute the fix (the monitor retarget above), it is Auto, not Assisted. - **External** — the app cannot perform it (for example, a driver install); it shows the exact required version and a deep link only. Example fix labels include "Switch to a supported codec for this container" and, for the HDR conflict, "Switch to AV1" / "Switch to HEVC". **Check catalog.** Checks cover recurring environment/config conditions: old driver, low disk, FAT32, unsupported codec on this GPU, audio-format mismatch, color-range (VLC) compatibility, refresh-rate vs CFR judder, the HDR10-vs-H.264 conflict, and an **exclusive-fullscreen window target** (`rec.capture.exclusive_window`): elevation-free detection that the selected window is in legacy exclusive fullscreen, which window capture records black. It is a **Notice** when only a fullscreen signal is present and a **Blocker** when capture has demonstrably produced no frames; its primary fix is the "Record the monitor instead" Auto retarget above. Diagnostics does not add runtime checks for already-fixed internal bugs. **Support channel (no telemetry).** Because ExoSnap sends no telemetry, the diagnostic artifact a user shares *is* the support channel. Three connected pieces make that shareable (see ADR 0044): - **Structured logs with a session key.** The engine writes a canonical JSON-lines stream that appends and rotates across launches, and every record carries a launch session id that also appears in the human-readable log's startup banner — one key ties the two streams together. - **Per-recording session report.** After each recording a `session-.json` is written beside the logs (10 most recent kept), capturing the resolved format, config, encoder init parameters, drop/dup/discontinuity counters, duration skew, A/V drift and peak drift, the segment list, and the failure phase. Metrics with no measurement read `"unavailable"`, never a fabricated zero. The session's frozen ledger (§11) — every entry that fired, with its occurrences — is a field of this report, so it rides along in the support bundle without a separate export. - **One-click support bundle.** A **Create support bundle** action (on Logs; also from the Diagnostics page) packages the rotated logs, the recent session reports, and GPU/adapter/display facts into a single scrubbed `.zip`. It is a neutral tool, not an error trigger — wording is "Create a diagnostic package to share with support", with no alarming styling. Nothing is uploaded: the file is saved via a dialog and revealed in the file manager. Paths, username and machine name are scrubbed, and capture-target window titles are redacted to `[capture-target]`. A **Startup** table on the Logs page shows start-to-milestone latencies so startup regressions are visible; it also rides along in the bundle. The Logs page states how much of the stream is currently shown ("Showing 22 of 22 entries · All · no search") as the page header's own subtitle, beside the title, rather than as a separate band between the toolbar and the list. The log list itself is deliberately full-width — a log line is data, and truncating it to protect a reading measure would be the wrong trade on a tool surface. It has one thin sticky header row, **Time | Level | Category | Message**, aligned to the data columns and without additional card chrome. **Time** is the time of day (`HH:mm:ss.zzz`); the date belongs to the exported history and the log file, which carry their own full stamps. **Present / tearing / latency diagnostics.** An opt-in, elevation-gated provider (PresentMon, the engine behind FrameView) enriches window/game-capture diagnosis and feeds judder correlation. A second in-process kernel trace on the same opt-in and the same elevation gate powers a DPC/ISR-latency check that names the offending kernel driver behind "smooth game, stuttery/crackling recording". It reports a peak only while it is measuring one: a trace that is stopped, refused or that ended by itself withdraws the reading and with it the recommendation, for the same reason the present figures do below. The app does not run elevated by default. The opt-in behind it is the Diagnostics header's in-depth switch (§11) and nothing else — session-scoped, off at every start, and changeable whenever a recording is not running; turning it on never relaunches the app by itself, it offers to (§11), so the switch's sub-text states whether the gate has actually been cleared: "On · not measuring · needs an admin relaunch" before the elevated relaunch, "On · elevated · PresentMon + DPC/ISR trace" once it has happened. The provider is never required, and the portable build degrades gracefully. The reported present figures always describe the **current** attribution window and nothing else. Starting a recording opens a window; ending it closes one, and the per-recording present, discarded and mode-flip totals reset rather than standing on the idle Diagnostics page describing a session that is over. The same rule covers the two ways a window can end without anyone asking: if the attributed process exits mid-recording, or the trace session itself dies, the reading goes back to unavailable. A stale number presented as a live one is worse than no number, because it is the only one a reader cannot tell apart from a measurement. --- ## 12. Settings model (Default / Expert) Settings targets a **technically experienced or technically inclined user who is not expected to be an audio/video specialist**. Default therefore keeps container and codec choices visible, together with frequent and useful recording controls, but presents curated, human-readable values. It does not hide the format of the file the user is creating. Expert reveals raw, specialized or unsafe mechanics in place. The Default quality control is the named five-tier quality choice; Expert replaces it with explicit rate-control and numeric fields. The **Default / Expert split** follows that audience contract rather than how technical a label looks. A row is Expert-only when at least one holds: misconfiguring it can produce an incompatible or broken file (**bit depth, color range, chroma subsampling, rate control**); it is meaningless without format expertise (**NVENC encoder preset, keyframe interval, Opus frame duration/complexity**); or it protects pipeline integrity and disabling it can ruin a long recording (**audio clock slaving**). Everything else is Default-visible even when it looks technical - mic gain, mic channel mode, audio bitrate, channel count, audio bit depth and FLAC compression, the brickwall limiter, microphone post-processing, and automatic split. The Expert toggle is a Settings-only control; Diagnostics does not read it and has no Expert view of its own (§11). Cards use this fixed two-column order. Left: **Recording format**, **Audio sources**, **Output**, **App behaviour**, **Hotkeys**, **Appearance**. Right: **Video quality & timing**, **Audio encoding**, **Webcam**, **Overlays**, **Updates**, **Support & diagnostics**. Frequently used rows lead within a card, and related rows may form calm visual groups. At the bottom of both Default and Expert, the two columns end within approximately one card height of each other. Expert mode adds, in place within their existing section: **Bit depth**, **Color range**, **NVENC encoder preset**, **Keyframe interval**, and **Chroma subsampling** (Recording format); the **Rate control** selector (CQ/VBR/CBR) with its **CQ** or **bitrate** spinbox, the free-entry **frame rate** field (1 fps up to the fastest monitor's refresh rate), and **Frame pacing** (Video quality & timing); **Opus frame duration**, **Opus complexity**, **Sample rate**, and **Audio clock slaving** (Audio encoding). **HDR handling** is Default, not Expert: HDR-capable displays are mainstream, the row is already display-conditional (Section 6), and its default (Tone-map to SDR) is the safe, universally compatible choice, so gating it further behind Expert added no safety. Within Expert mode, a narrower relevance gate still applies to two rows whose visibility depends on the active codec/GPU rather than on Expert mode alone: **Bit depth** (shown only for a codec that carries 10-bit — HEVC/AV1) and **Chroma subsampling** (shown only when the selected codec and the active GPU can carry 4:4:4 at all). A row that fails its relevance gate is not shown rather than shown-and-disabled, so the expert view lists only what currently applies; the Chroma row is the one exception that stays visible-but-disabled for a 10-bit conflict specifically, because that conflict is fixable in place (switch Bit depth back to 8-bit). **Audio is two sections, both Default-visible.** **Audio sources** carries which sources are captured and how the microphone is picked up (the three source rows, device, channel mapping, gain, post-processing); **Audio encoding** carries the shape of the resulting stream (bitrate, channels, sample rate, bit depth, FLAC compression, the brickwall limiter, and the Opus/clock expert rows). One card mixed capture routing with encoder tuning and grew past every other card in the page; splitting it also lets the two halves sit in different columns, which is what evens the page out once Expert adds its rows. The split is a section boundary, not an Expert gate: both cards are present in Default, and Expert still only reveals rows inside them. **No section is Expert-gated.** Every embedded Settings section, including **Support & diagnostics** (logging level, crash-report consent and support tools), is visible in both Default and Expert. Expert only reveals additional rows in place inside a section; it never hides or reveals a whole section. Locked rows retain readable labels and current values while their interaction chrome and secondary hints recede (§2.2). Settings offers inline info hints (hover popovers on info-i icons and the countdown chevron). Info-i placement is **selective**: only rows with a genuine A/B tradeoff carry the icon (plain boolean rows do not), and per-option helper text lives inside the popover rather than as a separate line under the control. The multi-option compare popovers are **explanatory only**: they list each option with its qualitative tradeoff and mark the currently active value, but the setting itself is always changed with the row's own control (the combo box next to the glyph), never by clicking inside the popover — the popover carries no second, reduced picker. Single-line info hints render as calm tooltips; longer ones use a short bold lead line with the explanation beneath. Roadmap-only controls may appear as honest, disabled "planned" rows to communicate direction without enabling unimplemented behavior. --- ## 13. Updates and crash reporting **Updates.** - **Off by default for every build, including official builds** (`check_updates_on_start` defaults to `false`, ADR 0045) — a first launch never contacts a server before the user turns the automatic check on from the Settings update card. Self-built binaries additionally never run the check at all, via a separate compile-time gate (`IsUpdateCheckEnabled()`), regardless of this setting. Both a manual "Check now" and a toggleable automatic update check exist; a manual check is itself the user's explicit action and needs no separate consent step. - **Stable** and **Preview** channels. The selection **takes effect immediately** — the next check queries the channel now shown, not the one that was selected at launch — and it **invalidates the previous channel's answer**: "Update available — " describes a feed, not the application, so switching returns the card to **Unchecked** rather than carrying the other channel's verdict over. Switching does **not** start a network check on its own; a check remains the user's explicit action. A check already in flight when the channel changes is discarded rather than presented under the new channel. - The client verifies the manifest against a **detached ed25519 signature** (Monocypher; shipped as a sibling `update-manifest.json.sig` asset, verified over the exact received manifest bytes before any field is parsed) plus each package's **SHA-256** hash, and **refuses downgrades**. No GitHub token is used by the client. No update is performed during recording or finalization, and the app never restarts silently. - **UI home:** the update UI lives on the **Settings update card**, plus a **dedicated updater window** (per design canon `Updater.html`). The earlier About-overlay placement is superseded, and the hidden legacy `UpdateSettingsPanel`/`AboutOverlay` compatibility shim has been removed — the Settings card is the **only** update state model. - **Version identity.** The app knows its **full release version** (e.g. `0.9.0-rc4`) everywhere: runtime `kVersion`, the handoff document's `currentVersion`, manifest `version`, About page, update card, support bundles and crash metadata. RC builds are therefore honestly older than the final of the same base version, and an older RC naturally discovers a newer RC on the Preview channel. Developer builds identify as `-dev` and never impersonate a release. - **The update that was offered is the update that gets installed.** Applying hands the updater a single versioned handoff document naming exactly one release, plus the signed manifest that proves it; the updater verifies that signature itself before reading any manifest field and installs that version or nothing (ADR 0068). It never resolves a release of its own. If the offered release's manifest could not be fetched, the update stays visible — a newer release that exists must not be reported as "up to date" — and applying refuses with that reason instead of starting an updater that cannot work. - **Update card states** (normative): **Unchecked** (`No update check has run yet.`, button `Check for updates`; the state at launch and after a channel switch — deliberately distinct from "Up to date", which would be an assertion the app has not earned) · **Up to date** (`✓ Up to date · `, button `Check for updates`) · **Checking** (`Checking for updates…`, action disabled; the click never moves the page's scroll position or steals focus out of the card) · **Available** (`Update available — `, button `Update to ` launches the real external updater for Portable and MSI) · **Scoop** (`Managed by Scoop — update with 'scoop update exosnap'`, button `Open releases page`; the swap updater never touches a Scoop tree) · **Updater running** (`Updater running… follow the updater window`, action disabled, while the detached updater is open and the app still owns its normal lifetime) · **Pending** (`Restart pending… finishing the update`, button disabled, only after the updater's marked close/handoff request has been accepted) · **Error** (honest message, button `Retry`). An updater that exits or fails before handoff returns the card to an actionable Available/Verification-reinstall state. Pending is never reconstructed from a persisted UI flag on a later normal launch. - **Verification reinstall mode.** Starting the app with `--verify-update-reinstall` enables a **non-persistent** test mode that offers exactly one extra action: reinstalling the **identical** full version (exact string match) through the complete production path — official feed, channel selection, embedded key, signed manifest, package SHA-256, recording/finalizing guards, real updater handoff, swap/install, verify, relaunch and cleanup. The card then shows `Verification reinstall available — ` with `Reinstall ` and the notice *Reinstalls the currently running signed version.* The mode can never offer a downgrade, never relaxes signature/hash checks, is logged in the app log and support bundle while active, and remains explicit in the updater content — the eyebrow above the version pills reads `REINSTALLING EXOSNAP`, and the working copy says so too (for example, `Downloading version again…` and `Reinstalling version …`) — rather than through a title-bar badge. It is gone after the next restart. Without the flag, the same version is never offered. - **The offered version is the installed version.** The version the card offers is handed to the updater as its **pinned target**, and the updater installs **that** version or nothing at all: the signed manifest must name it byte-for-byte (the same exact-string equality the verification reinstall gate uses, because SemVer equality collapses foreign prerelease labels). If a newer release appears in the feed between the app's check and the updater's own resolution, the run stops before a single package byte is fetched, with `The offered version is no longer what the channel serves` and the installation untouched; a fresh check is the way forward. Without this, the offer, the "What's new" notes written for the next launch, the applied-version loop guard and the build actually installed could each name a different version. The **What's new** payload is bound to the same pinned target for the same reason. - **Manual updater start.** `exosnap-updater.exe` ships next to `exosnap.exe` and is double-clickable, so starting it by hand is a real entry point — and the only way back when a failed update has left the app unable to start. Started **without arguments** it opens its normal window at rest and works out its own context (installed vs. portable, the install directory, and the version actually on disk); it never checks, downloads or installs anything on its own. The flow is *Idle → Checking → **Up to date** | **Update available** → Downloading → **Ready to install** → Installing → Verifying → Launching*, with a confirmation at each bold step: **Check for updates**, then **Download update**, then **Install now**. "Nothing newer" is a **result** (`ExoSnap is up to date`), not a download error. A build that may not contact the feed says so plainly and contacts nothing. The handoff started by the app is unchanged: it enters through its arguments and still runs start-to-finish without asking, because the user already confirmed in the app. - **A cancellation is not a failure.** Stopping a download on purpose leaves the updater in a **Canceled** state — neutral card, no error tone, the stopped step back to *queued* rather than marked failed, and the truth that matters spelled out: *nothing was installed and nothing was changed*. It carries no failure case and offers no retry, because there is no fault to re-enter; a manual run offers a fresh **Check for updates**, a handoff run offers only **Close** (its confirmation was given in the app). Cancellation is only honoured where the engine actually observes it — while a package is downloading. The feed check and the wait for ExoSnap to close take no cancellation at all, and the install/verify/relaunch steps must not be interrupted; in all of those the request is refused rather than accepted and ignored. - **The updater's exit code is its outcome.** `0` update applied and verified (including the case where only the automatic relaunch did not open), `1` the update failed, `2` the command line could not be understood, `3` a manual check found nothing newer, `4` installed and Windows must restart to finish, `5` canceled, or closed before any outcome. It used to be `0` for every run that reached the event loop, including a visibly failed one, so nothing that launched the updater could tell the two apart — and a cancellation must not report `1`, which would send a release script looking for a fault that never happened. - **Development feed override.** `exosnap.exe --update-base-url ` points the update check at a named feed instead of the production one, so a development build can exercise its own check and the full app→updater handoff. It is **refused in an official build**, refused on anything that is not an https URL with a host, never persisted, and the same URL is handed to the updater — app and updater must resolve one feed, not two. It relaxes nothing else: the recording guard, the signature verification and the package hash are unchanged, and a development build's pinned key is all zeros, so no manifest from any feed can pass verification there. - **Shipped flow:** the update check (automatic or manual) finds a new version → an "update available" notification deep-links to the Settings update card → clicking **Update** opens the dedicated updater, a separate process that performs every step itself. Its step list (as rendered) is: 1. **Downloading update** — fetches the signed manifest and the package (progress %); the ed25519 manifest signature and the package SHA-256 are verified within this step, and a mismatch aborts and keeps nothing. 2. **Closing previous version** — waits for the app to exit (the running image is locked). 3. **Installing new files** — swaps the files in place: portable does a staged rename (old → backup, verified new → live); an installed build runs `msiexec` (one UAC prompt). 4. **Verifying installation** — confirms the installed version; on failure the backup is restored. 5. **Launching ExoSnap** — relaunches the app on the new version; on a healthy start the backup is discarded. The swap is **staged and reversible** (dual-swap) and nothing is swapped until verification passes. The updater uses one fixed, non-resizable **520 × 680 logical-pixel** window in every working and terminal state. Its 56 px title bar follows the main ExoSnap window's geometry and identity: the standard icon and two-tone **exosnap** wordmark at the same inset and scale, followed by a stable, neutral **Updater** role label, plus 46 × 56 **Minimize** and **Close** controls with the main window's hover/pressed treatment. A disabled maximize control is not shown. Phase, warning, error, completion and verification-reinstall state are not duplicated in the title bar; they remain in the progress/status and result content that explains them. Below the fixed step list, every state uses the same 110 px three-row state panel and the same separate 36 px action row, so status copy and buttons never move vertically. Result actions use the same compact button component: retryable results show **Retry/Re-download + Close**, a completed update that needs a manual launch shows **Open ExoSnap + Close**, and non-retryable results show **Close** only. The ambiguous **Open current version** label is not used. The updater remains a Qt Widgets executable in this product wave. A later rendering-only Qt Quick port may not redesign it: the fixed 520 x 680 geometry, layout, visual output, state model, accessibility contract, automation surface and behavioral/visual tests remain compatibility requirements. The window shows **one** progress language at a time. Until the run has measured something — the pre-flight frame it opens on — the ring carries **no percentage and no arc**, and the status line under it says what is happening; a percentage appears only once there is real progress behind it, and the ring never changes size between the two. The **eyebrow** above the version pills names what kind of run this is (`UPDATING EXOSNAP`, `REINSTALLING EXOSNAP`, or after a terminal failure `EXOSNAP WAS NOT UPDATED`), because the title bar's role label is the same word in every state. **Version emphasis follows what is actually installed.** While a run is in flight — and after one that applied — the target version carries the accent. After a terminal failure it does not: the emphasis moves to the version that is on the machine, so a failed update can never read as "the new version is installed". **No text is ever cut off at a widget edge.** Version identifiers come from the release manifest and installer detail text comes from Windows Installer, so neither has a length the window can assume. Anything too long is shortened with an ellipsis and keeps its full value on the tooltip and for screen readers: version pills shorten in the **middle** (the build suffix identifies which build it is), prose shortens at the **end**. The 520 × 680 window never grows to fit long content, and long content never pushes the action row out of view. During **Downloading** and **Closing previous version**, the normal title-bar close action safely opens the same confirmation as the visible **Cancel update** action. The confirmation says that download/preparation progress is discarded while the installed version remains unchanged; only the explicit destructive confirmation cancels the worker and closes the updater. While the swap-critical steps (Installing, Verifying, Launching) are in flight the updater window **refuses to close** — the close control is disabled and Alt+F4, the taskbar close, and a Windows logoff are all ignored, while the fixed action row shows a disabled **Close** action and a concise non-interruptible hint — so the in-place rename cannot be torn apart mid-swap. If a forced kill (or a power loss) does interrupt a portable swap between its two renames, the **next** update run detects the orphaned backup and restores the last-known-good install before proceeding. After a successful update the downloaded manifest, signature, and package are removed from the temp directory; a failed run keeps them so a retry can reuse them. Failure is shown as one of three variants. The headline, user-oriented detail, safety line, icon and actions live inside one status-tinted result card; raw WinHTTP/MSI/path diagnostics stay in logs rather than appearing as a second loose UI error. Retry/Re-download actions carry a refresh icon. Each variant **always names the version that is safe to run right now**: - **Amber (retryable):** a pre-swap step failed (download, app would not close, install could not start) or elevation was declined; the **current** version is intact and the step is retryable. - **Red (hard stop):** the card names the safe state explicitly — a failed download verification stops before anything is installed ("nothing was installed"); a failed installation verification restores the backup ("your previous version was restored"); a failed `msiexec` run reports the installer's own result ("your previous version is still usable"). - **Green (installed and verified, only the auto-relaunch didn't start):** the **new** version is live and safe — the updater offers a manual start rather than presenting it as an error. Two moments are unavoidably not fully in-app: the UAC prompt (installed builds only) and the brief window while the app is closed for the swap. - **What's new (shipped).** Release notes are surfaced from the GitHub release bodies already present in the `/releases` payload the update check fetches — no extra network call. One in-window overlay shows the notes as a single, always-expanded scrolling document (no collapse/expand), newest first; bodies are Markdown with their links in the accent colour, and a footer **"All releases"** link (bottom-left, with an external-link icon) opens the releases page; a primary **Close**/**Got it** button sits bottom-right. It has two entry points, which differ in *which* notes they show: - **Pre-update:** while the Settings update card shows "Update available — vX.Y", a **"See what's new in vX.Y"** link opens the overlay with the **full reference list for the active channel** (every non-draft release; Preview includes release candidates, Stable does not) — not just the pending gap, so the list itself doesn't go empty when there's nothing new to update to. - **Post-update (one-time):** clicking **Update** persists the gap notes — every version in `(installed, target]` — as a pending payload; on the first launch of the new build — when the payload's target equals the running version and the suppress setting is off — the overlay is shown once with those gap notes and the payload is cleared. This mode carries a **"Show release notes after updates"** checkbox, **checked by default**, persisting `whats_new_suppressed` (unchecking it suppresses future auto-shows); that setting only gates the post-update auto-show and never hides the card link. First install, downgrade, and manual-ZIP updates leave no matching payload, so no overlay appears. **Crash reporting.** - **Opt-in and consent-gated**, local-first (out-of-process Crashpad). Nothing leaves the machine without an explicit current or previously remembered choice. - **A crash always leaves a local minidump.** Official builds capture it out-of-process via Crashpad; builds without it fall back to an in-process handler. The dump stays on the machine and is never uploaded without consent. - **Next-launch only** — crashes are offered for reporting on the following launch. - **Single explicit delivery path:** automated upload to **Sentry with EU data residency** is compiled in only for official builds and remains consent-gated — so self-built binaries never upload. The crash dialog does not construct or open a prefilled GitHub issue. - **Crash-dialog actions:** `Send report` (when the official upload path is active), `Don't send` and a directly visible tertiary `Open crash folder`. There is no overflow menu. - The dialog leads with the truthful next-launch fact ("the previous session did not shut down normally"), recovery availability, dump availability, cause availability and the non-empty version/encoder context from the previous-session sidecar. It does not render empty exception/module/thread/stack rows or substitute current-machine OS/GPU probes as crash facts. - Privacy details are a collapsed, keyboard-accessible disclosure. They distinguish the privacy-scrubbed structured event from the native minidump binary, including the dump's possible loaded-module paths and IP transit to Sentry's EU ingest region. - **Persisted policy:** `Ask every time` (default), `Send automatically`, or `Never send`. Migration maps legacy `auto_send_crash_reports=true` to `Send automatically`; false or a missing key maps to `Ask every time`, never to `Never send`. `Never send` suppresses only the report consent prompt; the independent local recording-recovery surface remains available. - The dialog checkbox is **Remember this choice for future crashes**, unchecked by default. It is **always visible** — at the 860 × 700 minimum window, with the report-contents disclosure expanded, the contents scroll and the checkbox does not, because a consent decision whose control has scrolled off screen is one the user can commit without seeing. It is draft state only: Send + remember commits `Send automatically`; Don't send + remember commits `Never send`; either action without remember leaves `Ask every time`; close, Escape and backdrop dismissals never change policy. Settings → Support & diagnostics → **Crash reports** exposes all three choices and applies the corresponding consent state immediately. - A Send action without remember is one-shot consent. The app releases and flushes the pending Sentry envelope, then resets sentry-native consent to unknown, so later reports in the same session do not inherit that decision. Remembered automatic send remains persistent until changed. **Signing status.** Builds are **not yet code-signed** (portable ZIP and MSI); Windows SmartScreen may warn on first launch. ExoSnap participates in the SignPath Foundation free code-signing program; release binaries will be signed once the certificate is issued. --- ## 14. Privacy - **No analytics, no telemetry, no account.** By default ExoSnap makes **no network connections** — including on first launch, for every build. - Only two features can contact external services, each strictly opt-in and only when the user acts: the **update check** (public GitHub Releases API, no auth token; sends only the request's IP and a fixed User-Agent identifying the checker — **no ExoSnap version number is sent**, the newest-release comparison is entirely client-side) and **crash reporting** (Sentry, EU data residency). - **Crash-report allowlist (what is sent):** | Tag key | What it carries | |---|---| | `os.name` | Windows edition name (e.g. "Windows 11") | | `os.version` | Windows build/version string | | `gpu.model` | GPU adapter name | | `gpu.vendor` | GPU vendor (e.g. "NVIDIA") | | `gpu.driver` | GPU driver version | | `app.version` | ExoSnap version | | `encoder_backend` | Active encoder backend (e.g. "nvenc") | | `container` | Output container (e.g. "mkv") | | `video_codec` | Selected video codec | | `audio_codec` | Selected audio codec | plus the crash stack/minidump — nothing else. Only `encoder_backend`/`container`/`video_codec`/ `audio_codec` are populated on the Sentry path today; `os.*`/`gpu.*`/`app.version` are allowlisted (so a future change could set them) but are not populated by current app code. Kept honest by `scripts/validate-privacy-allowlist.ps1` (see `docs/privacy-review.md`). - **Never sent (structured event):** usernames, file paths (including output folder and recording filenames), machine name, breadcrumb logs. Recording content is never captured. No persistent device identifier is created. **Exception:** the Crashpad minidump binary (uploaded on a hard crash, separately from the structured event above) carries the full install path of `exosnap.exe` in its module list — for a portable install run from under `%USERPROFILE%` this can include the username segment of that path. See `PRIVACY.md` and `docs/privacy-review.md` for the full explanation and the recommended mitigation (install to a path with no username component). - A capture-target **window title** is never written to the on-disk log in the first place (a `[window]` placeholder is logged instead) and is additionally redacted if it ever reaches the one-click support bundle. - Settings, presets, recording history, recovery manifest, logs, and recordings are stored locally (under `%LOCALAPPDATA%\ExoSnap\`) and never transmitted; the user can delete any of them at any time. --- ## 15. Platform support and known boundaries - **Windows 10/11 x64 only** (Windows 11 primary, Windows 10 best-effort). No ARM64, Linux, or macOS. - **NVIDIA NVENC only** for video encoding (RTX 20-series or newer recommended, current driver). AMD AMF, Intel QSV/oneVPL, and software (CPU) encoding are **not** available and are not implied. - Requires the **Microsoft Visual C++ 2022 x64 Redistributable** (bundled as a declared dependency by the WinGet package; not bundled by MSI, portable ZIP, Chocolatey, or Scoop). - Distributed as portable ZIP and MSI (both unsigned for now). - Not present in current builds: Replay Buffer; chapter export from the Edit/Output/Save surface (Quick Trim and markers are implemented and reachable; container chapter export is deliberately out of scope for the MVP); HDR beyond BT.2020 (HDR handling now covers both monitor and WGC window/game capture via an FP16 frame pool; no HLG/wide-gamut is the confirmed 1.0 scope); 4:2:2 chroma and 10-bit 4:4:4 (8-bit 4:4:4 for H.264/HEVC is implemented as an Expert option); multi-vendor hardware encoding; immediate in-session crash reporter; the fullscreen/borderless/exclusive game-capture matrix. The in-place dual-swap updater has since shipped (see Section 13) and is no longer on this list. **Licensing.** ExoSnap is GPL-3.0-or-later and bundles FFmpeg as LGPL-2.1-or-later shared libraries (dynamic linking). --- ## Resolved-decision notes - **Default audio state.** The canonical default is **context-aware**, per Section 3: the `APP` row is always present in the Settings Audio card, is a persisted setting, and defaults enabled — but it only actually contributes audio while a specific application window is the capture target; for a screen or region capture it recedes and no `APP` source enters the recording. For screen capture the shipped default is `SYS` enabled and `MIC` present but off. Each enabled source is a separate resulting track (per `CLAUDE.md`). An older internal preset note described a System-only default with no `MIC` row at all; Section 3's context-aware default is authoritative.