The AArch64 -> C emitter and the ArmRecomp bridge that hosts it.
Instructions translated beyond the original set: EXTR/ROR, ADC/ADCS/SBC/SBCS,
LDPSW, the load/store exclusives including the pair forms, CLREX, PRFM/PRFUM as
the hints they are, MRS/MSR for FPCR/FPSR, CNTPCT/CNTVCT/CNTFRQ, CTR_EL0, and
the DC cache-maintenance family.
Three of those were not gaps but wrong answers:
Exclusives used to become a plain load/store with STXR unconditionally
reporting success, which is exact only when nothing else can touch the
address. Under real guest threads that makes every compare-and-swap
non-atomic. They now route through Core::ExclusiveMonitor, the same monitor
the fallback JIT uses, so a lock taken in recompiled code is visible to a
thread on the JIT.
FPCR/FPSR had nowhere to live, so a guest rounding mode was silently reset to
the host default and dropped again on every transition to the JIT.
The counter and CTR_EL0 are read from the emulator's own sources rather than
invented, so the two engines cannot disagree about time or cache geometry
across a transition.
Values that are constants here (CNTFRQ, CTR_EL0) and operations with nothing to
do (the DC clean/invalidate family - suyu never sets Dynarmic's
hook_data_cache_operations, so the JIT compiles them to nothing too) are
answered inline. DC ZVA and the IC family deliberately still fall back: one
writes memory, the other invalidates the instruction cache, which suyu acts on.
Coverage instrumentation on both sides. Static: a per-module
recomp_coverage.json with emitted/unhandled counts grouped by encoding and
signature. Runtime: blocks executed, SVCs, transitions in each direction split
by cause, and histograms of unimplemented opcodes, uncovered PCs and SVC
numbers. The static histogram turned out to be a poor predictor of what
executes - the three largest runtime gaps found so far appeared nowhere in the
static top twenty - so the runtime figures are what the work is prioritised
against.
The static recompiler is AArch64-only, so whether a title is A32 or A64 decides
whether it can be targeted at all. Answering that by booting is slow and, for
titles that fail to boot, impossible.
--probe-isa-list walks XCI -> secure NSP -> Program NCA -> ExeFS -> main.npdm for
each path in a list file and writes a TSV. One process handles a whole library:
no Vulkan, no emulation, no crash risk.
The Program NCA is looked up under TitleType::Update as well as Base. An
update-only NSP has no Base entry at all, so it came back "no exefs" and its
NPDM was never read - which matters because an update can change the answer:
the target title ships an ARM32 base whose 4.0.0 update is AArch64, and
reading only the base reports the whole title as untargetable.
Size is not an identity. the target title's 2.4.0 and 4.0.0 updates both have
a 1476-byte main.npdm, one ARM32 and one AArch64, so a log line printing only the
size cannot say which of them a given code path loaded. Treating size as an
identity sent an investigation into which update gets applied down six dead ends;
an FNV-1a of the file bytes separated them in a single run.
The NPDM line also carries the ISA up front, which decides whether a title can
use the AOT path at all, and the ACI title id.
PatchExeFS logs which ContentProviderUnion slot answered for the update. The
union checks SysNAND, UserNAND, SDMC, FrontendManual, External in that order and
the frontend fills FrontendManual with the booted file's own NCAs, so an on-cart
update can answer even when a newer one is installed. Naming the slot is the
difference between knowing that and guessing at it.
PatchManager replaces the ExeFS wholesale when an update is present, so the code
the emulator actually runs is the update's. The AOT exporter read the base NCA
regardless, and the resulting image diverged from live execution the moment an
updated title was booted - on a second title, at block 153.
Look up the Program NCA under the update title id and TitleType::Update first,
falling back to the base when no update exists.
RegisteredCache::GetFileAtID tries eight layout variants and skipped every odd
index except 7:
if ((i % 2) == 1 && i != 7) continue;
The .cnmt.nca suffix was therefore only ever tried at the cache root, never
inside a 000000XX/ directory - which is exactly where meta NCAs are stored, per
GetRelativePathFromNcaID's own format string for that case and per what
InstallEntry writes. So every meta NCA in NAND was unreachable, ProcessFiles
bailed at `file == nullptr` before registering anything, and no installed update
or DLC ever entered the cache.
Silently, too: a miss is indistinguishable from nothing being installed, which
is why the frontend's installed-title listing reports zero.
With the cache empty, PatchManager::PatchExeFS fell through the
ContentProviderUnion to the FrontendManual slot - which the frontend fills with
the booted file's own NCAs - and applied whatever update the cartridge happens
to carry, ignoring anything installed.
Fixing that exposed a second defect immediately behind it. ProcessFiles keys its
metadata map by title id, so two installed versions of the same title collide,
and the winner was whichever the directory walk reached last - no version
comparison anywhere. Now the higher GetTitleVersion() is kept.
Found on the target title, which ships an ARM32 base with an ARM32 on-cart
update while its AArch64 update sits in NAND: the title booted 32-bit and the
installed update was simply invisible.
The registration map used emplace, which is a no-op when the key already exists.
A process re-registering its RomFS therefore kept the stale entry rather than
replacing it, and the title panicked on boot.
insert_or_assign is what was meant: the newest registration wins.
RegisterHandlers passed a FunctionInfoTyped<T> array straight to
RegisterHandlersBase, which walks it as FunctionInfoBase[]. The two layouts do
not match. sizeof() agrees, so a size assertion passes and tells you nothing,
but the member offsets differ: dumping IpcController's table showed entry 0 with
its name at offset 24 and entry 1 with its name at offset 20, with entry 2's key
landing at byte 60 rather than 64. Every element after the first was read from
the wrong place.
The result was silent and severe. IpcController registered 2 of its 6 handlers,
because the corrupted keys collided and emplace kept only the distinct ones.
QueryPointerBufferSize (command 3) was among the lost, and it is part of CMIF
session setup - so every title stalled during early service initialisation and
never reached the graphics stack at all.
Index with the typed pointer instead, one element at a time, via a new
single-entry RegisterHandlerBase. The array forms are kept for callers that
genuinely have a FunctionInfoBase array.
Two defects in the recompiled-image dispatcher, either of which alone makes the
AOT path execute nothing.
recomp_image_lookup subtracts the module base itself, so the dispatcher was
passing an already-relative PC and the subtraction happened twice. Every lookup
underflowed and missed, and the whole image silently ran on the JIT instead.
The module-name table had only four entries {rtld, main, subsdk0, sdk}. Switch
load order is rtld, main, subsdk0..subsdk9, sdk, so any title with more than one
subsdk mismapped: the module at index 3 is subsdk1, but a four-entry table hands
it the sdk image, and every module from index 4 on got no image at all. Super
a second title loads nine modules - four were left uncovered and one was given the
wrong image.
The owner search now also picks the image with the greatest base not exceeding
the PC, rather than asking every image in turn. Asking each in turn silently
returned a different module's block whenever two modules defined something at
the same offset, which they always do at offset 0.
Only the Linux .so was produced, so no suyu_libretro.dll ever reached the release. RetroArch loads .dll on Windows and .so on Linux, so both are shipped.
autofix.ci opened PRs referencing pre-rewrite history, and every extra workflow auto-fired on push, creating runs attributed to the pusher. ci.yml also hardcoded the original repository name.
All build jobs were using dist/ as staging dir, which collides with
the repo's own dist/ folder containing resources/icons/translations.
This caused libretro tar.gz to contain 625 resource files instead of
just suyu_libretro.so. Renamed libretro artifact for clarity.
Rename constructor params that shadow member variables in
location_descriptor.h, a64_location_descriptor.h,
microinstruction.h, and terminal.h.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CPM downloads boost with per-library include directories but boost_headers
only pointed at its own empty include dir. This adds all libs/*/include
dirs and creates Boost::boost alias for dynarmic compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Metal renderer is WIP — removing the enum value avoids -Wswitch errors
in 11+ files. The MTL window class still compiles on Apple but isn't
selectable at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add externals/metal-cpp/CMakeLists.txt (INTERFACE lib for Metal C++ headers)
- Wire metal-cpp into externals/CMakeLists.txt under if(APPLE)
- Add Metal to RendererBackend enum (used by suyu_cmd on Apple)
- Suppress unused 'props' warning on macOS in emu_window_sdl2_vk.cpp
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>