docs(app): emulation thread phase 5 plan, qualify the stable default (#1039) (#1129)

Gates G1-G5 before releases default the emulation thread On: threaded
twins for the 20 app tests without one, every CI platform, the Studio
60 Hz measurement, Windows/Linux/120 Hz reports and a two-week nightly
window. Three PRs: 5a twins, 5b evidence record, 5c the flip (last).
The inline loop is retired only after a default-on release ships.

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Joe MattielloandClaude Opus 5.5 authored and GitHub committed 2026-10-31 15:01:58 -04:00
1 parent 06fa8a7193
commit 43383a050a
1 file changed
+106
+106
View File
@@ -0,0 +1,106 @@
# Emulation thread, Phase 5 plan (qualify the stable default)
Issue: #1039 (part of #967 / #254). Design of record: [app-emulation-thread-design.md](app-emulation-thread-design.md), section 8 "Rollout". Earlier phases: [1](app-emulation-thread-plan-phase1.md), [2](app-emulation-thread-plan-phase2.md), [3](app-emulation-thread-plan-phase3.md), [4](app-emulation-thread-plan-phase4.md).
**Goal:** the 4.0 release turns Settings > Interface > Emulation thread **On by default**, on evidence, with the inline loop still selectable. This phase is mostly qualification. The only behaviour change is the last PR, which flips the release default.
**Not in this phase:**
- **Retiring the inline loop** (`stepEmulation`, `Pacer`, the inline branch of `runInteractive`). #1039's rule replaces the phase 4 plan's "Retiring the inline loop (phase 5)": remove it only after a stable release has shipped default-on without regressions. That follow-up issue is filed when the evidence exists, not before.
- **Changing Frame pacing's default.** It stays Jaguar rate unless decision 1 below says otherwise.
## Where it stands (2026-10-31, develop @ fe80b36a)
- **Phases 1–4 are merged.** Nothing is switched off while threaded: `no_threaded_limits` enforces this statically in CI. Mode B is in (#1125, #1128).
- **Nightlies default On since phase 3d** (about 2026-10-24); releases and `dev` builds default Off. No threading issue has been filed since. That is a week with no reports, not qualification evidence.
- **Headless runs stay inline whatever the default** (`app.cpp`, the `emuThreadThisRun_` decision). Flipping a channel's default therefore changes no existing test's mode, and the inline tests keep covering the fallback.
- **CI coverage today:**
- 82 tests carry `LABELS emu_thread`. They run in all seven `app.yml` configurations: macOS arm64, Linux x86_64 (dev and nightly channel), Linux aarch64, Windows x86_64 MinGW, Windows ARM64, and MSVC (experimental).
- The TSan job runs `ctest -L emu_thread` on Linux. On the merged mode B code, 82 of 82 pass, re-run on the Studio.
- **Windowed coverage today:** the Studio's 60 Hz panel (macOS) only. Headless CI never has vsync, so the late latch and mode B are exercised on Windows and Linux only by unit tests with fake clocks.
## Gates
Each gate lists its evidence. Gates 1–3 are work in this repo; gates 4 and 5 are evidence that has to come in. The flip (PR 5c) opens only when every gate is met or the maintainer waives it in writing on #1039.
| Gate | Met when | Evidence goes to |
|---|---|---|
| **G1 coverage** | Every headless test that runs the app with content has a `_threaded` twin, or a line here saying why it is mode-specific | PR 5a |
| **G2 platforms** | G1's twins pass on every `app.yml` configuration: the six required ones, plus MSVC, which may stay experimental. TSan stays green | PR 5a CI |
| **G3 Studio, 60 Hz** | The phase 4 measurement passes its bar (below) and the transition runs recover. The phase 1 gate (`emu_thread_gate_check.sh`) passes on the 4.0 candidate | #1128 comment, then the evidence table here |
| **G4 other displays** | At least one windowed report each for Windows, Linux and a 120 Hz panel, threaded, with no regression against inline | #1039 comments |
| **G5 nightly window** | Two weeks of nightlies with no open threading regression; any report is fixed or shown not to be a threading issue | #1039 |
**G3 bar** (phase 4, Task 8):
- Mode B event→present within ~2 ms of inline on every title, Cybermorph included.
- No periodic repeat in mode B.
- No new underruns.
- CPU within +5% of mode A.
- PAL reports the `ratio` fallback.
Mode A must not regress against the phase 1 numbers.
### G1: tests without a threaded twin
Taken from `ctest --show-only=json-v1` at fe80b36a. These are tests whose command runs `virtualjaguar` with content and passes no `--emu-thread`, and that have no `_threaded` twin.
| Area | Tests | Twin |
|---|---|---|
| Smoke | `smoke_cd`, `smoke_boot_cart_rom`, `smoke_disc_insert_cart_hidden`, `smoke_bad_option`, `smoke_locked_option` | add |
| Cheats | `cheat_state`, `cheat_apply_control`, `cheat_persist_{save,restart,control}`, `cheat_tempest_{lives,control}`, `cheat_community_install` | add. The persist chain needs its own data directory and fixtures |
| Overlay | `controls_overlay_{default,keyboard,dismissed,rebound}` | add |
| Netplay | `netplay_pair_shared` | add (the other pair tests already have one) |
Mode-specific by design, so no twin:
- `gdb_inline_halt*`, `neuro_app_inline`, `netplay_pair_mixed` (one peer each way), `emu_thread_default_smoke_*`.
- `tv_menu_headless` and `tv_script_windowed_only`: a headless TV script always runs inline (`app.cpp`).
- `native_menu_dump`: no content.
- `netplay_discovery`: skipped in CI.
The `*_e2e` tests (`input_e2e`, `devices_e2e`, `bios_e2e`, `debug_scan_e2e`, `debug_op_e2e`) drive `CoreHost` directly, not the app loop. Their threaded counterparts are `core_host_thread` and `emu_thread_e2e`.
**Shutdown.** These already cover it:
- `emu_thread_unit` covers shutdown races.
- `gdb_threaded_halt` covers SIGTERM mid-halt.
- `debug_e2e_threaded` covers the 3 s join while halted.
5a adds one headless case: quit while a slow load `call()` is in flight (`VJ_TEST_SLOW_CALL_MS`, as `smoke_busy_threaded` uses). It must exit 0 within the join timeout with no owner violation.
## PRs
- **5a `test(app)`: threaded twins (G1, G2).**
- The twins listed above go through the registration each area already uses: the `_threaded` loop in `VJEmuThreadTests.cmake`, the twin helper in `VJAchTests.cmake`, or the same pattern added to `VJCheatTests.cmake` and `VJSmokeTests.cmake`.
- Each twin carries `LABELS emu_thread`, so the TSan job runs it.
- Each twin must reach the same pass regex as its inline original. A twin that needs a different expectation is a finding, not a test tweak.
- Fix any real failure in its own PR, outside 5a.
- Refs #1039.
- **5b `docs(app)`: qualification record.**
- The evidence table below, filled from G3–G5.
- A short "Reporting a threading problem" note in the README's "Emulation thread" section: how to report (the About window's version and channel line, the perf note, `--emu-thread off` to compare).
- One pinned comment on #1039 asking nightly users for G4 reports.
- Refs #1039.
- **5c `feat(app)`: releases default the emulation thread On (the flip, last).**
- `emuThreadDefaultFor("release")` returns true. `VJ_EMU_THREAD_DEFAULT_VALUE` follows in `app/CMakeLists.txt`, and the `static_assert` in `build_channel.h` and the `emu_thread_default_unit` / `emu_thread_default_smoke_*` expectations change with it.
- Unchanged:
- a stored `ui.emu_thread` choice still wins;
- `--emu-thread off` still runs inline for one run;
- a failed thread start still falls back inline.
- Docs: design doc section 8 "Shipped (phase 5)" and the README.
- The 4.0 release notes say how to turn the thread off.
- Closes #1039 (and #967 if the maintainer agrees that the retirement follow-up is filed separately).
## Decisions for the maintainer
| | Question | Recommendation |
|---|---|---|
| 1 | **Frame pacing default:** keep Jaguar rate for 4.0, or default nightlies to Match display now so feedback accrues? | Default **nightlies** to Match display after G3 passes; keep Jaguar rate for the 4.0 release. Mode B's only windowed evidence is one 60 Hz macOS panel. |
| 2 | **`dev` channel after the flip:** Off (today) or On? | **On**, with 5c. Phase 3 kept `dev` Off so PR artifacts behave like releases; after the flip that same reason means On. |
| 3 | **G4 without volunteers:** if no Windows, Linux or 120 Hz report arrives, does the flip wait? | Wait for **Windows** (the largest user base), or waive in writing on #1039. Linux and 120 Hz can ship on unit-test coverage plus the inline fallback. |
| 4 | **G5 length:** two weeks of nightlies? | Two weeks, starting when 5a merges, so the twins have run on every PR in that time. |
## Evidence
Filled in by 5b. One row per run: build ID, host, display, arm, title, numbers, and a raw-trace path under `vj-eval/`.
| Date | Build | Host / display | What | Result |
|---|---|---|---|---|
| | | | | |