You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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 flags — tvos-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.
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:
pthread_create/sthread_createanywhere)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.hcounts events, not time ("blitter called 131,628 times"). It cannot say where milliseconds go.BENCH_PROFILE=1, a non-shipping build flavour.docs/profiling.mdis entirely host-side — Instruments,perf, flame graphs. None of it reaches a locked-down tvOS device running RetroArch.make benchmarkis 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 providesperf_register/perf_start/perf_stop/perf_logand renders the results in its own performance-counter UI, on tvOS included.Scope:
retro_init, tolerate a frontend that declines itdocs/profiling.md(currently host-only)Exit criterion: a per-subsystem millisecond breakdown captured on the A10X, not inferred from a Mac.
Phase 2 — optimise what measurement actually implicates
Deliberately unordered; Phase 1 decides the ranking. Candidates, each to be confirmed before work starts:
tvos-arm64compiles at-O2with no LTO (the-fltopath atMakefile:147does not apply to it). Cheapest possible A/B, may be free. NoteBUILD_AXESmust gain any new switch or the object stamp goes stale.JaguarRead*/JaguarWrite*address-decode chain. A page-table dispatch is the classic fix.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, anddram_scale_sweep.sh.Verification requirements
Any change landing under this epic must clear the gates that already exist:
make testexits 0; bothtest_audio_clippingandtest_audio_presence(clipping alone misses the silencing-regression class)test/regression_test.shscreenshots unchangedtest/tools/dram_scale_sweep.shfor anything touching the GPU/68K timing modelmake benchmark— never a cross-machine comparisonReported by @jmattiello from tvOS RetroArch testing.