Skip to content

Performance epic: profile and optimise for older/slower platforms (tvOS A10X) #509

Description

@JoeMatt

Problem

Performance on older Apple silicon is poor — reported on an Apple TV 4K (A10X) in RetroArch, where the core struggles while the same device runs Dreamcast (flycast) comfortably.

That comparison is the useful clue, and the gap is architectural rather than a regression:

flycast (Dreamcast) virtualjaguar
CPU emulation SH4 dynarec (JIT to native ARM64) pure interpreter
Engines per frame SH4 + JIT'd geometry 68000 + GPU RISC + DSP RISC + OP + blitter, all interpreted
Threading multi-threaded single-threaded (no pthread_create/sthread_create anywhere)

So on a 3-core A10X we saturate one core doing interpretation of five engines. Newer devices brute-force through it; A10X-class hardware does not. This is CPU and memory-bandwidth bound, consistent with the report.

Why we can't act on this yet

We have no way to profile on the device. Current tooling:

  • src/core/perf_counters.h counts events, not time ("blitter called 131,628 times"). It cannot say where milliseconds go.
  • Those counters are gated behind BENCH_PROFILE=1, a non-shipping build flavour.
  • docs/profiling.md is entirely host-side — Instruments, perf, flame graphs. None of it reaches a locked-down tvOS device running RetroArch.
  • make benchmark is a same-host x86_64/arm64-Mac metric and explicitly not comparable across machines.

Optimising interpreter internals without device data risks spending a release cycle on the wrong engine.

Phase 1 — on-device measurement (blocks everything else)

Wire RETRO_ENVIRONMENT_GET_PERF_INTERFACE (env 28). RetroArch provides perf_register / perf_start / perf_stop / perf_log and renders the results in its own performance-counter UI, on tvOS included.

Scope:

  • Register the perf callback in retro_init, tolerate a frontend that declines it
  • Scoped counters around the real per-frame consumers: 68K slice, GPU slice, DSP slice, OP halfline render, blitter launch, DAC/audio mix
  • Counters must compile to nothing when the frontend provides no interface — this ships in release builds, so zero cost when unused
  • Document the RetroArch-side capture recipe in docs/profiling.md (currently host-only)

Exit criterion: a per-subsystem millisecond breakdown captured on the A10X, not inferred from a Mac.

DONE — shipped 2026-08-20. Implemented in sub-issue #510, merged via PR #518
(src/core/perf_iface.{c,h}, env 28 registered in retro_init, runtime-gated on
vjPerfActive so BUILD_AXES is untouched, capture recipe in docs/profiling.md,
test/tools/perf_iface_witness.c wired into make test). Boxes were left unticked
here, which caused the work to be re-dispatched; ticking them now.

The epic's exit criterion is still unmet: a per-subsystem millisecond breakdown
captured on the A10X. Phase 2 stays blocked on that measurement.

Phase 2 — optimise what measurement actually implicates

Deliberately unordered; Phase 1 decides the ranking. Candidates, each to be confirmed before work starts:

  • Build flagstvos-arm64 compiles at -O2 with no LTO (the -flto path at Makefile:147 does not apply to it). Cheapest possible A/B, may be free. Note BUILD_AXES must gain any new switch or the object stamp goes stale.
  • Memory dispatch — every access walks the JaguarRead*/JaguarWrite* address-decode chain. A page-table dispatch is the classic fix.
  • RISC interpreter dispatch — GPU and DSP share an ISA and both cycle-step; computed-goto dispatch is the usual lever.
  • OP per-halfline cost on the render path.
  • Blitter already has SSE2/NEON (src/tom/blitter_simd_neon.c); check the NEON path is actually selected on tvOS/iOS arm64.

Explicit non-goals for this epic

  • No dynarec. Ruled out by maintainer decision, not deferred. Three reasons stack: iOS/tvOS enforce W^X, so a JIT cannot run on the very platform this epic exists for (App Store builds never receive dynamic-codesigning); it would be a multi-release rewrite with a large correctness surface across four processors; and the plentiful off-the-shelf m68k dynarecs target our cheapest engine, since the GPU/DSP RISC is a custom Jaguar ISA with nothing to borrow. Do not reopen this as part of a performance PR.

    Sanctioned techniques instead: measurement first, then computed-goto dispatch and other interpreter optimisations, SIMD where a hot loop admits it, and targeted bottleneck work. Everything stays an interpreter.

  • No accuracy regressions traded for speed. The timing-accuracy work (Timing accuracy: unified plan — menu 2x root cause, then one correct system with no toggles #408, milestone v4.0.0) sets the correctness floor; anything here must hold make test, the audio pair, and dram_scale_sweep.sh.

Verification requirements

Any change landing under this epic must clear the gates that already exist:

  • make test exits 0; both test_audio_clipping and test_audio_presence (clipping alone misses the silencing-regression class)
  • test/regression_test.sh screenshots unchanged
  • test/tools/dram_scale_sweep.sh for anything touching the GPU/68K timing model
  • Same-host before/after via make benchmark — never a cross-machine comparison
  • Re-measure on the A10X. A win on an M-series Mac proves nothing about the platform this epic exists for.

Reported by @jmattiello from tvOS RetroArch testing.

Metadata

Metadata

Assignees

Labels

enhancementNew capability or improvementepicMulti-release tracking issue; closed when every sub-issue shipsperformanceSpeed, frame time, memory bandwidthsize: XLEstimate: extra large (multi-release / epic)

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions