* test(recompiler): fail when shared images skip the ABI
LoadRecompiledImagesFrom accepts a library that only exports
recomp_image_lookup. suyu-cmd packs missing DLLs into a vector and
assigns bases by that vector's index.
exporter_smoke requires a versioned ABI, a matching context prefix, a
content hash, a required set_base, and a mapping that keeps main and sdk
at their own bases when rtld is absent.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): validate a versioned shared-image ABI
Shared images export RecompImageAbi with version, GuestContext prefix
size, module index, and NSO build_id. ValidateImageExports rejects a
library that only has recomp_image_lookup. PlaceLoadedModule keeps holes
when an earlier module is missing, and ApplyModuleBase uses the declared
index so later bases do not shift.
LoadRecompiledImagesFrom and suyu-cmd call that contract before they
install lookup or set_base. EmitProject writes the ABI. game_export
passes the NSO build_id and static registration includes the abi pointer.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* chore(recompiler): stop calling lookup the only image export
Generated recomp_export.c still said lookup was the one symbol the host
resolves. Loaders now require recomp_image_abi as well.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): keep unknown image names out of rtld's slot
EmitProject and game_export stored ModuleIndexForName misses as 0, which
is rtld. Unknown names now use kRecompMaxModules so PlaceLoadedModule
rejects them. SlotByName's nn prefix strip and the Qt cmake-folder walk
are named helpers. Leftover load-order, preferred-path, and nn-prefix
comments are gone.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* test(recompiler): fail when icache invalidation keeps AOT
ArmRecomp::ClearInstructionCache and InvalidateCacheRange are empty after
the JIT fallback exists. The smoke test encodes the intended policy:
stop selecting invalidated AOT blocks, do not chain into them, and
forward the invalidate to the JIT.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): reject AOT and notify JIT on icache invalidate
ClearInstructionCache and InvalidateCacheRange were empty after the
fallback JIT existed, so guest executable writes kept stale AOT and
JIT translations. Reject AOT selection and direct chains after an
invalidate, and forward the same call to ArmDynarmic64 when it exists.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): reject AOT after nested JIT icache halt
Guest IC is unhandled in AOT, so it runs on the fallback JIT. That
callback clears only ArmDynarmic64. Translate HaltReason::CacheInvalidation
and Clear AOT in RunFallback so lookup cannot re-enter stale blocks.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* chore(recompiler): drop unused icache policy wrappers
RecompICache only rejects AOT. Range forwarding stays on ArmRecomp,
which still calls the fallback JIT with the kernel's address and size.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* test(recompiler): fail when module bases outlive the process
ArmRecomp::RunThread keeps a function-static bases_registered flag. A later
process therefore skips SetRecompBaseSetter after stop/start, restart, or a
title switch, and coverage counters keep the previous run.
This adds RecompSession with that same process-lifetime flag and a smoke
check that ASLR remaps and title switches re-register bases.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): scope module registration to the guest process
Replace the function-static bases_registered flag with RecompSession.
AttachProcess resets registration and coverage when the process changes.
EnsureModuleBasesRegistered waits on a mutex so cores share one setter call.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* chore(recompiler): delete RecompSession comment sermons
Drop the class lifetime lecture, mutex narration, constructor and
destructor recap, and the discarded ModuleBaseFor alibi. SPDX and the
AttachProcess return contract stay.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* test(recompiler): fail when unresolved imports fake a return
Move slot targeting and trap handling into unresolved_import.h so
exporter_smoke can call the same policy ArmRecomp uses. The test requires
JUMP_SLOT and IRELATIVE to use the halt sentinel, and requires the trap
to halt with the symbol name instead of returning X0=0 to the caller.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): halt on unresolved imports instead of fake returns
Unresolved JUMP_SLOT and unsupported IRELATIVE write kUnresolvedImportTrap
only. The dispatcher and Dynarmic fallback stop with PrefetchAbort and a
diagnostic that names the recorded symbols. Guest RET stubs are no longer
used as import targets.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* chore(recompiler): drop added unresolved-import comments
Remove the header banner, sentinel/slot sermons, namespace closer, and
IRELATIVE alibi this branch introduced. Move FindGuestReturnStub into
exporter_smoke so the production header is not a test scanner.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* test(recompiler): fail when generated FP ignores FPCR
Exporter smoke now compiles translated FADD under guest RMode RP and RM.
1.0 + 2^-53 must differ across those modes, or the instruction must be
unhandled so Dynarmic can apply FPCR. MSR/MRS still have to round-trip
the control registers.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* fix(recompiler): leave host-C FP arithmetic unhandled
kTranslateHostFpArithmetic defaults off, matching the fixed-point
conversion flag. FADD, FMUL, FDIV, FSQRT, FCVT, SCVTF, FADDP, vector
FADD/FMLA, and by-element FMUL emit recomp_unhandled so Dynarmic can
apply the stored FPCR. FMOV, FABS, FNEG, FCMP, and MRS/MSR stay
translated.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* chore(recompiler): drop added FPCR comments
Remove the sermons this branch introduced around the unhandle flag,
encoding constants, and the smoke banner. Keep the flag and the tests.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Repair generated C placement, fix RET/BLR semantics, and add a portable exporter smoke/regression target to CI.
Move AES helpers to file-scope constant tables so EmitProject output is valid C, make RET use encoded Rn and BLR read its target before writing X30, and add a header-only exporter smoke job that compiles a generated project and keeps those branches as regressions.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Fix exporter smoke on Windows by spawning CMake argv instead of cmd.exe.
cmd.exe /c stripped quotes around C:/Program Files/CMake, so nested configure never ran. Invoke cmake and the branch probe with _spawnv, and reuse the parent generator/platform.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Quote Windows CMake argv for CreateProcess so generator names with spaces survive.
_spawnv still joined arguments unquoted, so Visual Studio 18 2026 became generator Visual and Program Files split off cmake.exe. Build a CommandLineToArgvW-compatible command line instead.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Add project-local verify-suyu skill for the Qt frontend.
Teach agents to build suyu with the flags this tree needs, launch an isolated
-gamer instance, and drive library/mode/settings flows over the localhost MCP
JSON-RPC helper instead of clicking unlabeled Qt widgets.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Stop tracking control-suyu pycache.
The helper is a Python script; compiled bytecode does not belong in the skill.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
* Ground verify-suyu on the live Ubuntu 24.04 build.
Use system FFmpeg (bundled 8 needs vaMapBuffer2), fall back to /proc for
MCP port ownership when ss is missing, and note that set_gamer_search_filter
updates MCP state without filling the search box.
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Dallin Bland <SourCreamCulture@users.noreply.github.com>
Reverts the mk8-recomp fork's version bump to v0.0.5. PROVENANCE.md is left as-is since it documents that fork's own real fixes and versioning rationale as a historical record.
devel/libusb was deprecated into the FreeBSD base system in 2013 and has produced no binary package since; pkg install failed with 'No packages available to install matching libusb'. FreeBSD 15+ already provides the libusb API in base, which is what the project's own CMakeLists.txt comment already assumes.
PatchRomFS applied an update only when the NCA loaded and produced a
RomFS. When it did not - the file missing, locked by another process, or
failing to decrypt - neither branch ran and nothing was written to the
log at all. The base game booted unpatched, which is indistinguishable
from having no update installed: the boot succeeds, and the only outward
sign is a version in the window title that a user has nothing to compare
against.
Both failing paths now log an error naming the title and version, and
record it for the frontend, which raises a dialog saying the game is
running unpatched and why it usually happens.
The frontend check sits in UpdateStatusBar rather than next to
UpdateWindowTitle: a title asks for its RomFS well after the window title
is set - around fifty seconds into loading on the one this was found on -
so a check at boot always ran before there was anything to report.
ConsumeUnappliedUpdates clears what it returns, so it fires once despite
the 500 ms cadence.
Two installed versions of one title collide in the index, and the newer
one wins. The log said which version was kept but nothing about which
files the loser owned, so finding the dead copy - a whole extra program
NCA, 4.5 GB in the case that prompted this - meant deleting a candidate
and seeing what broke.
Now the skipped meta's own NCA id and every content record it points at
are logged, which is enough to remove the duplicate by hand and know
that nothing still in use went with it.
It asked the provider for ContentRecordType::Program against each title
type. Add-on content is registered as Data, not Program - AddOnContentManager
and PatchManager both filter AOC that way - so every DLC installed in NAND
was missing from the listing, and the absence read as "no DLC installed"
rather than as a gap in the query. Now the union of both record types is
taken per title type.
The name was blank for those too. Neither an update nor an add-on carries
its own control data, so the name comes from the base title, but an
add-on's id is base + 0x1000 + index while an update's is base|0x800 -
masking off the low bits alone lands on a title that does not exist.
It built its path from QStandardPaths::GenericDataLocation, which is
%LOCALAPPDATA% on Windows while the NAND lives under %APPDATA%. So it
pointed at a directory that does not exist and returned count 0 no matter
what was installed. Even with the path right it listed the content-index
folder names - 00000002, 0000001D - rather than any title.
Moved to main.cpp, where the filesystem controller is in scope, and asked
the content provider the emulator actually boots from: every title type,
with title id, type, version and the name from the base title's control
data. It also reports the unfiltered record count, so a listing that does
not account for everything in NAND is visible rather than silent.
Version formatting is byte-wise, matching FormatTitleVersion in
patch_manager.cpp - that one is file-local, so the layout is repeated
here rather than guessed at. Guessing it first gave v0.1.6 for what is
actually v0.22.0.
Keys, firmware, a game and an export each already had a menu entry, but
they sat in two different menus in no useful order and nothing reported
which were done, so the way to find out what was missing was to boot and
read the failure.
File -> Setup lists the four in dependency order with their real state.
It owns no logic: each step carries a predicate over the checks that
already existed - ContentManager::AreKeysPresent, CheckFirmwarePresence,
the game list model, the exporter's remembered output roots - and a
QAction that is already wired up, so the dialog only draws the answer and
triggers the action. Every row refreshes after any action returns,
because installing firmware can change what the key check reports.
Compiles and links; the dialog itself has not been opened yet.
The build-to-binary format captured its compiler environment from a
literal "Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build/
vcvars64.bat", and the two cmake searches globbed the same tree. None of
them find anything on a machine whose install is a different edition, a
different year, or on a different drive - this one is 18/Community - so
cl.exe and link.exe stayed off PATH and compiler detection fell over.
vswhere ships with every VS installer since 2017 and is the supported way
to ask, so it answers for Community, Professional and Build Tools alike.
SUYU_VCVARS overrides it for layouts it cannot see, and a missing toolset
is now a log line that names the override rather than silence.
The generated build_native_windows.cmd had the same glob plus a latent
batch bug: it read %CMAKE_SEARCH% inside the parenthesised block that set
it, which expands empty without delayed expansion. Rewritten with goto,
so no delayed expansion is needed.
externals/ownfoil carries a nested submodule pointing at a1ex4/NSTools, and
that repository is gone - GitHub returns 404, so git falls through to asking
for credentials and `git clone --recursive` aborts before any of the tree is
usable. It fails for everyone, on every platform; the only reason it has not
been noticed is that an existing checkout never re-initialises it.
Nothing in the build refers to ownfoil. It is a separate web application that
was vendored in, not a dependency: no CMakeLists mentions it, and removing it
changes nothing that compiles. The other 38 submodule URLs all resolve.
Dispatch CPU share 19.5% -> 16.4%: recomp_lookup and recomp_image_lookup leave
the profile entirely. Wall clock is unchanged, which is what a 3-point cut in a
36% thread looks like against this noise floor.