Detail for the compact rules in CLAUDE.md. LLM-oriented shorthand.
make -j$(getconf _NPROCESSORS_ONLN) # build, auto-detects platform
make -j$(getconf _NPROCESSORS_ONLN) DEBUG=1 # debug: -O0 -g
make clean
make platform=ios-arm64 # cross-compileOutput: virtualjaguar_libretro.{dylib,so,dll}. CI: make -j4 on Ubuntu (GCC) + macOS
(Clang) + test/regression_test.sh. Makefile = 30+ targets, auto-detected via uname
or platform=; Makefile.common lists sources. Flags: -D__LIBRETRO__, -DMSB_FIRST
(big-endian).
The rpi* platform names identify the silicon exactly, so they carry real per-SoC
tuning (issue #560). Everything is resolved in the Makefile's rpi* platform block —
never re-specify these on the command line, and never append an -O in a platform
block (that is the #516 bug: the last -O wins and a later -O2 silently beat
-Ofast for years).
| platform | SoC | core | blitter | flags on top of -O3 |
|---|---|---|---|---|
rpi0 rpi1 |
BCM2835 | ARM1176JZF-S (ARMv6) | scalar | -marm -mcpu=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard |
rpi2 |
BCM2836 | Cortex-A7 | neon | -mcpu=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard |
rpi3 / rpi3_64 |
BCM2837 | Cortex-A53 | neon | -mcpu=cortex-a53 (+-mfpu=neon-fp-armv8 -mfloat-abi=hard on 32-bit) |
rpi4 / rpi4_64 |
BCM2711 | Cortex-A72 | neon | -mcpu=cortex-a72 (same 32-bit addition) |
rpi5 / rpi5_64 |
BCM2712 | Cortex-A76 | neon | -mcpu=cortex-a76 (same 32-bit addition) |
Three traps, all verified in an arm-linux-gnueabihf container:
-mfpu/-mfloat-abiare 32-bit-ARM only.aarch64-linux-gnu-gcchard-errors (unrecognized command-line option '-mfpu=neon-fp-armv8'), so a*_64row that acquires one does not build at all.rpi0/rpi1need-marm. Debian's armhf cross toolchain is--with-arch=armv7-a+fpand defaults to Thumb; ARMv6 only has Thumb-1, and GCC cannot pair Thumb-1 with hard-float (sorry, unimplemented: Thumb-1 'hard-float' VFP ABI). A native Pi 1 compiler defaults to ARM mode, so this bites cross-builds only.rpi1is scalar on purpose. ARM1176 has no NEON. Do not "fix" it.
-Ofast is deliberately not used: it implies -ffast-math, which #517 measured at
~0% here and removed globally as a determinism hazard (run-ahead #400, netplay,
savestate compat all depend on determinism). make OPT_LEVEL=-Ofast platform=rpi4
still works if you want to measure it.
test/tools/simd_matrix_check.sh (in make test) asserts all of the above — blitter,
the last -O, the expected -mcpu, and that 32-bit-only flags stay off 64-bit rows.
Release/nightly build all eight via release.yml's matrix, cross-compiled on x86_64
(-mcpu names a core to generate code for; it does not require running on one).
The libretro GitLab buildbot cannot do this:
libretro-infrastructure/ci-templateshas norpi*template (onlylinux-aarch64.ymlfor ARM Linux), so per-chipset Pi cores would need a template added upstream.
Host builds: prefix DEVELOPER_DIR=/Library/Developer/CommandLineTools. xcode-select
points at Xcode.app, so bare make/cc resolve inside an app bundle → macOS raises an App
Management prompt on every invocation (dozens per multi-agent run). Same Apple clang, no
bundle, no prompts. Do NOT xcode-select --switch globally — full Xcode is needed for iOS
cross-builds.
libretro buildbot uses MSVC on Windows; CI has a c89-lint job. Run
bash scripts/c89-lint.sh src/YOURFILE.c before pushing.
- No mid-block declarations — all vars at top of block, before any statement. Most common violation.
//allowed (GNU89) but prefer/* */for new code.- No C99: no
for (int i…), no compound literals, no designated initializers, no VLAs. - Exempt (see
scripts/c89-lint.sh::skip_file):src/m68000/cpu*.c,src/m68000/read*.c(UAE 68K);src/bios/jag*bios*.c(bin2c hex tables);src/tom/blitter_simd_{sse2,neon}.c(intrinsics);test/tools/test_rcheevos_e2e.c;test/tools/flicker_detect.c;deps/libchdr/*andtools/jagcd/*(vendored libchdr = C99 unity TU, seeunity.orule).
make links production-slim ABI (retro_* only). make test needs the wide test ABI so
harnesses can dlsym internals. TEST_EXPORTS=1 selects wide and adds -DVJ_TRACE →
changes object content, not just export list.
Switching either direction is automatic: Makefile stamps build config into .build-config;
when it differs it deletes the library and every object before building. Both
make → make TEST_EXPORTS=1 test and the reverse work with no make clean. Cost: one
full rebuild per flip (~21s at -j8 without ccache).
Stamp covers every compile-affecting switch (BUILD_AXES: TEST_EXPORTS BENCH_PROFILE DEBUG BLITTER_TRACE COVERAGE RELEASE_DEBUG_INFO DEBUG_PRESENTATION STATIC_LINKING platform OPT_LEVEL LTO IOS_MCPU).
Adding a new CFLAGS-affecting switch means adding it to that list — forgetting = silent
chimera binary, not a build error. It stamps variable names, not $(CFLAGS) (DEBUG=1
injects a per-second -DBUILD_TIMESTAMP, so stamping flags would flush every build).
The flush is unconditional, not a curated "objects that use vjtrace" list — that list is what
broke before (omitted src/tom/blit_memo.o → make TEST_EXPORTS=1 then plain make died on
undefined _vjtrace_emit from BlitMemoLaunch; reverse linked clean but silently kept no-op
macros). Do not reintroduce a curated/grepped/-MD-derived list; a file can pick up VJT_*
via indirect include. make -n is exempt from the flush (dry run costs nothing). CI gates both
directions on every host row of c-cpp.yml.
History: before v2.3.2 the switch didn't relink at all (Missing: m68k_execute). Before #457
the stamp tracked TEST_EXPORTS alone → make DEBUG=1 after a release recompiled zero objects
(a "debug build" that was all -O2, no debug info); toggling BENCH_PROFILE recompiled nothing
so timing_probe reported timing_halfline_callbacks counter not found (reads as broken tool,
not ignored flag). VJ_EXPECT_BUILD can't catch either — git rev is identical across the flip.
Every ELF/GNU-ld release build (unix, rpi*, generic arm64/aarch64/armv*, Android
via jni/Android.mk) now compiles with -fno-semantic-interposition and, for
non-TEST_EXPORTS builds, -fvisibility=hidden. Mach-O targets (osx, ios*, tvos*)
are untouched — Apple ld64's two-level namespace already resolves same-image symbols
directly, so GCC's ELF-only interposition assumption never applied there. Gated on
GC_STYLE=gnu in the Makefile, the same variable that already marks the GNU-ld targets
for --gc-sections — but GC_STYLE=gnu is a slightly wider set than "ELF", so two
platforms are explicitly carved back out:
-
qnxalso hasGC_STYLE=gnuand keeps-fvisibility=hidden(GCC ≥ 4.0), but is excluded from-fno-semantic-interposition(needs GCC ≥ 5.1): no CI or buildbot job buildsqnxin this repo, so its toolchain floor is unverified, and QNX SDP 6.x'sqccis known to wrap an old enough GCC that the flag can hard-error instead of no-op. -
win(native MinGW) also setsGC_STYLE=gnupurely because MinGW'sldhonours--version-script/--gc-sectionstoo — not because its output is ELF.win'sTARGETis a PE/COFF.dll, andlibretro.h'sRETRO_APIexpands to__attribute__((__dllexport__))there, not the visibility attribute, so this whole block's rationale (ELF-style GOT/PLT interposition,RETRO_APIsurviving-fvisibility=hiddenvia inherited visibility) doesn't apply — andwinis built in CI (c-cpp.yml's MSYS2 job,release.yml). It is excluded outright from all three of-fno-semantic-interposition,-fvisibility=hidden, and theLTO=1knob below. -
Why: without these flags, GCC treats every cross-file global (
gpu_reg,dsp_pc, the flag/counter externs the hot interpreter headers share) and every cross-file call (JaguarReadLong&c.) as potentially interposable, forcing GOT/PLT indirection per emulated instruction. This project already pins its entire exported surface via$(LINK_SCRIPT)(link.T/link-test.T), so nothing outsidelibretro.c'sretro_*functions is meant to be interposable in the first place. -
TEST_EXPORTSis exempt from-fvisibility=hidden: the white-box harnessesdlsym()internal symbols (see Test ABI section above); hiding them at compile time would break that even though the version script still lists them.-fno-semantic-interpositionstill applies underTEST_EXPORTS=1— it only affects codegen, not the symbol table. -
retro_*stays exported under-fvisibility=hidden:libretro.hdeclares every entry pointRETRO_API, which expands to__attribute__((visibility("default")))on non-Windows GCC/Clang ≥ 4. A visibility attribute already present on an earlier declaration governs the later definition, solibretro.c's definitions (which never repeatRETRO_API) inherit it from the header they include. -
LTOdefault-on for GNU-ld ELF: unix,rpi*, genericarm64/aarch64/armv*now pass-fltounless you opt out withLTO=0.win(PE/COFF) andqnx(unverified gcc floor) stay excluded even withLTO=1. Mach-O (osx/ios*/tvos*) stays default-off; explicitmake LTO=1on those is honoured so a macOS host can A/B the flag.classic_armv7_a7is unaffected: it already runs its own-flto=4 -fwhole-programpipeline in its platform block. Host A/B numbers are indicative only — Linux/Pi CI +test/tools/rpi_perf.shon device is the final arbiter.LTOis inBUILD_AXESsince it changes object content.COVERAGE=1force-disables LTO (even an explicitLTO=1): GCC's gcov instrumentation turns the dsp.c/gpu.c computed-goto dispatch tables into.data.rel.localarrays of.Llabel addresses that don't survive LTO partitioning (undefined reference to.L106'` at link). -
iOS/tvOS
-mtune:ios-arm64adds-mtune=apple-a10,tvos-arm64adds-mtune=apple-a8. These are ISA-safe (scheduling only) for RetroArch's iOS 9 / A7 and tvOS Apple TV HD / A8 floors. Do not default-mcputhere — that would emit CRC32 (A10+) / LSE (A11+) and SIGILL on the floor chips. Opt-in pin for iOS 17+ deploys:make platform=ios-arm64 IOS_MCPU=apple-a12(appends-mcpu=$(IOS_MCPU);IOS_MCPUis inBUILD_AXES). -
Android (
jni/Android.mk):ndk-buildnever includesMakefile, onlyMakefile.common, so it inherited none of the desktop optimisation flags.COREFLAGSnow also carries-O3 -DNDEBUG -fvisibility=hidden -fno-stack-protector -fomit-frame-pointer -fno-semantic-interpositionby hand. No-marchextensions (single-APK arm64-v8a baseline isarmv8-a). No-flto: the chip-tuning note did not propose it, and this host cannot runndk-buildto verify. -
Issue #516 does not apply here: neither
-fvisibility=hiddennor-fno-semantic-interpositionis an-O, so neither can silently shift a platform's resolved optimisation level.
Every harness that dlopens the core prints the binary's embedded version (vX.Y.Z <gitrev>[-dirty], also logged at retro_init). If VJ_EXPECT_BUILD is set (as make test
and cd_boot_matrix.sh do, via scripts/build-id.sh), a core whose version doesn't
token-match fails the load loudly instead of silently testing stale code.
VJ_EXPECT_BUILD=$(./scripts/build-id.sh) ./test/tools/your_harness ...scripts/build-id.sh prints short git rev + -dirty when tracked files are modified;
scripts/gen-version-h.sh stamps that same string into the core, so both sides agree. Exists
because make can skip a rebuild when mtimes are second-identical. Manual fallback:
nm -gU <dylib> | grep <newsymbol>.
Stale .o hazard: git stash cycles can build chimera binaries VJ_EXPECT_BUILD cannot
catch (git rev identical). Baseline measurements from a pristine worktree.
SDL3 + Dear ImGui + GL 3.3 desktop app; C++17 lives ONLY under app/, the core stays C89.
Separate CMake build (cmake -S app -B app/build, see app/README.md); the Makefile is
unaffected. app/cmake/VJCore.cmake PARSES the source list from Makefile.common (no second
list to keep in sync) and compiles all three blitter_simd_<arch>.c with
-DBLITTER_SIMD_AUTODETECT (as Package.swift does). Smoke tests are headless (SDL dummy
drivers, no GL): ctest --test-dir app/build -C Release; if an intentional emulation change
moves the yarc frame hash, regenerate VJ_SMOKE_YARC_HASH in app/cmake/VJSmokeTests.cmake.
CI: .github/workflows/app.yml. Full-core MSVC is unproven (#679), so that row is non-gating.