Files
virtualjaguar-libretro/docs/app-emulation-thread-plan-phase5.md
Joe MattielloandClaude Fable 5.1 7637eeda23 feat(app): every build channel defaults the emulation thread On (#1039 5c) (#1134)
The 4.0 flip from the phase 5 plan: emuThreadDefaultFor() is true for
release, nightly and dev (an unknown channel string stays Off), and
VJ_EMU_THREAD_DEFAULT_VALUE is 1 for every channel.  The channel still
names the build in About and the log.  Unchanged: a stored ui.emu_thread
wins, --emu-thread off runs one start inline, a failed thread start falls
back inline, headless runs stay inline unless --emu-thread on.

Called by the maintainer with G1-G3 met and G4/G5 still open; the phase 5
plan and the design doc's section 8 record that.  The default smoke tests
now expect "default on" in every channel; the unit test pins all three
channels On and unknown ones Off.

Refs #1039, #967.

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-01 01:30:39 -04:00

119 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
**Status (2026-11-01):** 5c shipped. Every channel (`release`, `nightly`, `dev`) defaults On. The maintainer called the flip with G1–G3 met and G4/G5 still open (see the evidence table); any G4/G5 report now lands against the default-on build.
**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 3 Studio 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).** *Shipped 2026-11-01, with `dev` On too (decision 2).*
- `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. *Taken: 5c sets all three channels 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, then kept up to date as G3–G5 evidence arrives.
Studio = the Mac Studio (M4 Max, 16 cores) on its 60 Hz panel, every windowed run launched through LaunchServices (#1026), 60 s per run with scripted input, started only at a 1-minute load under 4 (the host's iCloud daemons hold about 2.7 cores). Arms: off = inline; A = threaded, Jaguar rate; B = threaded, Match display. Event→present is the exact figure, about 11,100 samples per run. Traces are in `vj-eval/1039-g3/`.
| Date | Build | Host / display | What | Result |
|---|---|---|---|---|
| 2026-10-31 | 06fa8a71 (#1130) | GitHub CI, every `app.yml` configuration | **G1, G2:** the threaded twins | **Met.** 21 twins; `emu_thread` label 82 → 102 tests. All seven app configurations pass, MSVC included, and so does the ThreadSanitizer job |
| 2026-10-31 | a661028c (fe80b36a's code) | Studio | **G3 bar:** off / A / B / B / A / off on yarc, Cybermorph, yarc PAL (`mb-m3-*`) | **Met.** B − off event→present: yarc −0.18, Cybermorph +0.16, PAL +0.99 ms. 0 underruns in 18 runs. CPU against A: −9%, −6%, +1.5%. PAL reports `ratio`. Full tables: [#1128 comment](https://github.com/libretro/virtualjaguar-libretro/pull/1128#issuecomment-6545542038) |
| 2026-10-31 | a661028c | Studio | **G3 transitions,** Match display, yarc (`mbt-t3*`) | **Met.** sleep: B → A (no_vsync) → B, 0 underruns. fs: locks to B. link: both copies `mode A (link)`. halt (5 s, then Continue): ends B locked, ticks dropped 0, **1 underrun** across the halt |
| 2026-10-31 | 1b18d491 (phase 1) vs 43383a05 (develop) | Studio | **G3 "mode A vs phase 1":** A and off, yarc and Cybermorph, builds in order p1 dv dv p1 (`mb-p5-*`) | **Met: no mode A regression.** A − off event→present, p1 → dv: yarc +1.96 → −1.04 ms, Cybermorph +1.94 → +2.47 ms (dv's two runs: +1.5, +3.4). Mode A repeats in 60 s: yarc 26, 16 → 3, 1; Cybermorph 25, 19 → 64, 58. Inline Cybermorph rose as much (21, 15 → 46, 51), with the same change in frame cost in both arms (p1 4.8 → 3.0 ms, end-due spread 5.3 → 7.5 ms): the core or content changed between the builds, not the thread. A ÷ off repeats is about 1.2 on both builds. Both builds' traces read by the same summary script |
| 2026-10-31 | 43383a05 | Studio | **G3 halt, inline arm** | **Not measurable.** Inline, a halt runs in the core's nested `haltedFrame` loop; `--perf-halt`'s Continue is in the outer loop, so it never comes. That nested loop still runs the UI, so the debugger's Continue button works. Run stopped after 1 h 40 min |
| 2026-10-31 | 43383a05 | Studio | **G3 halt,** A vs B (`mbt-p5-{A,B}-halt`) | **Met.** A 5 s halt with all eight Tools windows open, then Continue: 0 underruns at Jaguar rate and at Match display. Match display ends B (locked), ticks dropped 0. The m3 run's single underrun did not come back |
| 2026-10-31 → 11-01 | 43383a05 | Studio | **G3 phase 3 gate** (`emu_thread_gate_check.sh`, yarc; `p5-gate*`) | **Met on a quiet host,** to be run again on the 4.0 candidate. Two quiet runs pass: halted iteration p99 18.32 and 18.15 ms in part A, 18.35 and 17.57 ms in part B (limit 25 ms; phase 3 measured 18.0–20.7). Neuro ok. One run under load failed part A at p99 33.08 ms (10 of 281 halted iterations missed a vsync): the host's 1-minute load rose from 3.6 to 7.0 during it. One run never got a quiet host (load 89, Spotlight) and was skipped |
| | | Windows, Linux, 120 Hz | **G4** | Pending: request on #1039. Not waited for: the maintainer called the flip on 2026-11-01 (5c) |
| 2026-10-31 → 2026-11-14 | nightlies | | **G5** | Window open from the 5a merge |