feat(libretro): add buildbot CI recipe and complete core feature set#312
Conversation
Adds the .gitlab-ci.yml the libretro/RetroArch team requested in #311 to get RustyNES onto RetroArch's built-in core downloader, and closes out the concrete feature gaps found while researching it. Buildbot recipe (crates/rustynes-libretro): - .gitlab-ci.yml: Windows x64, Linux x64, macOS x64/arm64, Android (4 ABIs), iOS arm64, tvOS arm64 via the shared libretro-infrastructure/ ci-templates includes. Excludes webOS (no reference rust core uses it, crate Makefile has no webos branch) and Nintendo Switch/libnx (no rust-libnx*.yml template exists upstream at all) as unproven for a first submission, per the issue's "only include platforms that will build successfully". - The shared templates run a FIXED `cargo build --release --target <triple>` with no `-p` flag, then `mv lib${CORENAME}.<ext> ${CORENAME}_libretro.<ext>`. Two fixes were required for this to actually work: - `[lib] name = "rustynes"` in the crate's Cargo.toml (previously unset, defaulting to `rustynes_libretro` from the package name) -- without this, CORENAME=rustynes_libretro would double the `_libretro` suffix in the final artifact name, and CORENAME=rustynes would look for a source file that doesn't exist. The crate's own Makefile is updated to match (copies from the new `rustynes.<ext>` source name into the unchanged `rustynes_libretro.<ext>` final name). - `[workspace] default-members = ["crates/rustynes-libretro"]` in the root Cargo.toml, so the templates' unscoped `cargo build` builds only this crate instead of all 18 workspace members (in particular the frontend's wgpu/winit/cpal/wayland stack, which has no business on a headless cross-compile buildbot image). Confirmed safe: every CI workflow, this crate's Makefile, and every scripts/*.sh invocation already scopes with `-p`/ `--workspace`/`--manifest-path`. Fixed the 4 documentation snippets that showed a bare `cargo build` (docs/dev/CONTRIBUTING.md, SUPPORT.md x2, docs/dev/DEBUGGING.md) so copy-pasting them still builds the whole workspace/frontend as intended. - Also fixes the crate Makefile's macOS cross-target gap (the `osx` platform branch set no RUST_TARGET at all) by adding explicit x86_64-apple-darwin/aarch64-apple-darwin triples keyed off ARCH, falling back to native compile when ARCH is unset (unchanged local `make` behavior on macOS). Core feature completion (crates/rustynes-libretro/src/lib.rs): - RETRO_ENVIRONMENT_SET_MEMORY_MAPS: registers WRAM/SRAM/VRAM descriptors via a new `register_memory_maps` call at the end of `on_load_game` -- the memory-descriptor path RetroAchievements' rcheevos prefers over the legacy pointer API (kept unchanged alongside it, since RetroArch's own .srm persistence goes through it regardless). - FDS load-path fix: `.fds` content was previously always routed through `Emu::from_rom` (cartridge-only), so FDS loading silently failed despite `valid_extensions` advertising it. Now detected via the extension libretro's GET_GAME_INFO_EXT reports and routed to `Nes::from_disk` with a `disksys.rom` lookup in the frontend's system directory. - Disk-control interface: on_set_eject_state/on_get_eject_state/ on_get_image_index/on_set_image_index/on_get_num_images/ on_get_image_path/on_get_image_label, backed by the existing Nes::disk_side_count/inserted_disk_side/set_disk_side API (the same one the desktop frontend's F9 keybind uses), registered via enable_disk_control_interface() -- surfaces FDS multi-side swap in RetroArch's Quick Menu. - Native Game Genie cheats: on_cheat_set/on_cheat_reset backed by Nes::add_genie_code/remove_genie_code/clear_genie_codes (already excluded from serialized state, so no determinism impact). - get_fastforwarding-gated audio skip in run_single/run_dual: skips the f32->i16 interleave + batch_audio_samples push while RetroArch is fast-forwarding/rollback-catching-up. A modest, honest win -- rustynes-core has no mixer-bypass API, so APU synthesis itself (the dominant cost) isn't skipped. - `rust-libretro`'s `unstable-env-commands` feature is now enabled (required for set_memory_maps/get_fastforwarding; disk-control and cheats were already available without it). - Investigated but explicitly NOT attempted: a region/timing (NTSC/ PAL/Dendy) core option. rustynes-core parses and caches region into fixed CPU/PPU dividers once at construction with no post-hoc setter (a deliberate hot-path optimization per bus.rs's own comment) -- adding this needs real rustynes-core construction surgery, not libretro-side wiring, so it's left for a future, separately-scoped change. Also deferred: Zapper/Power Pad peripheral wiring (RETRO_ENVIRONMENT_SET_CONTROLLER_INFO) -- confirmed feasible (rustynes-core already exposes Nes::set_zapper/set_power_pad) but needs substantially more new surface (a controller-info table, on_set_controller_port_device, new lightgun/analog input polling) than this change's scope. None of the feature-completion items touch CPU/PPU/APU emulation logic or on_serialize/on_unserialize -- all are additive FFI/ presentation-layer wiring over already-existing, already-tested rustynes-core APIs, so AccuracyCoin/golden-vector determinism is unaffected by design. docs/libretro/advanced_features.md updated to describe what's actually implemented (the memory-maps/fastforwarding/FDS/cheats sections previously described aspirational designs, including an FDS RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO approach that was considered but not built in favor of the simpler system-directory BIOS lookup that shipped instead). Companion upstream PRs (per docs/libretro/UPSTREAM_SYNC.md): libretro/docs#1164, libretro/libretro-super#2021. Closes #311.
…tion
The prior commit's advanced_features.md update incorrectly implied
RetroArch's own address-poke cheat manager ('RetroArch Cheats') was
unsupported because on_cheat_set only decodes Game Genie syntax. Per
docs/guides/cheat-codes.md upstream, that mechanism ('RetroArch
Handled' cheats) never calls on_cheat_set at all -- RetroArch pokes
the core's exposed memory directly via the same get_memory_data/
get_memory_size pointer API RetroAchievements uses, which has worked
since that API was first implemented. Only 'Emulator Handled' (native)
cheats go through on_cheat_set, which is what the Game Genie decoding
actually adds.
There was a problem hiding this comment.
Code Review
This pull request completes the feature set of the rustynes-libretro core, adding direct memory mapping via RETRO_ENVIRONMENT_SET_MEMORY_MAPS for RetroAchievements, Famicom Disk System (FDS) loading and disk-control swapping, native Game Genie cheat support, and a fast-forward audio-skip optimization. It also adjusts workspace configurations and build scripts to support the Libretro buildbot. The review identified two potential robustness issues in the new libretro callbacks: a potential overflow panic in on_get_image_label when handling large disk indices, and a missing guard in on_set_eject_state that could attempt to insert a disk side on non-FDS games.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
Adds the GitLab CI recipe required by libretro buildbot to publish RustyNES in RetroArch’s core downloader, and fills out remaining libretro-core feature hooks by wiring existing rustynes-core capabilities into the FFI layer. It also adjusts workspace defaults and documentation so unscoped cargo build in buildbot only builds the libretro crate while developer commands still build the full workspace as intended.
Changes:
- Add top-level
.gitlab-ci.ymlusing libretro’s shared CI templates and align produced artifact naming via[lib] name = "rustynes". - Scope unqualified workspace builds to
crates/rustynes-libretrovia[workspace] default-members, and update docs/snippets to use--workspace(or-p rustynes-frontend) when the full build is intended. - Implement additional libretro integrations in
rustynes-libretro(memory maps, FDS load routing + disk control, Game Genie cheats, fast-forward audio push skipping).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
.gitlab-ci.yml |
Adds the libretro buildbot GitLab CI recipe using shared templates and sets CORENAME. |
Cargo.toml |
Sets default-members to the libretro crate so buildbot’s unscoped cargo build doesn’t build the full workspace. |
crates/rustynes-libretro/Cargo.toml |
Forces compiled library name to rustynes and enables rust-libretro env-command feature needed for new hooks. |
crates/rustynes-libretro/Makefile |
Aligns Makefile artifact naming with [lib] name and fills macOS target selection by ARCH. |
crates/rustynes-libretro/src/lib.rs |
Implements memory-map registration, FDS .fds routing + disk control, fast-forward audio skip, and Game Genie cheat handling. |
docs/libretro/advanced_features.md |
Updates documentation to reflect implemented advanced libretro features and behavior. |
docs/dev/CONTRIBUTING.md |
Updates contributor build/test commands to explicitly use --workspace. |
docs/dev/DEBUGGING.md |
Updates perf-build instructions to build the frontend crate explicitly. |
SUPPORT.md |
Updates user-facing build commands to explicitly build the whole workspace. |
CHANGELOG.md |
Documents the new libretro buildbot recipe and libretro-core feature completions under Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- on_get_image_label: bound the synthesized 'Side X' letter to index < 26 instead of letting `b'A' + index as u8` overflow u8 (a debug panic, a silent wrap in release) for any frontend-supplied index past 'Z'; falls back to a numeric "Side N" label past that. - on_set_eject_state: return false up front when disk_side_count() == 0 (non-FDS content) instead of silently no-op-ing and reporting success. Verified this was never an actual panic risk -- the `Mapper` trait's default `set_disk_side` is an empty no-op, only fds.rs overrides it -- but reporting false is more honest. - on_cheat_set: remove whatever Game Genie code was previously applied at a slot before applying a new one there, instead of just overwriting the index->code map entry. Without this, re-toggling a cheat slot with a different code left the old patch active alongside the new one. - register_memory_maps: give the VRAM/nametable descriptor its own named "PPU" RETRO_ENVIRONMENT_SET_MEMORY_MAPS address space instead of the blank/default one. Nametable RAM (CIRAM) lives on the PPU's own internal bus -- on the CPU's real bus, $2000-$2007 are the PPU MMIO registers, not video RAM, so the default-space descriptor was misrepresenting it. Matches libretro.h's own documented convention for other separate buses (SNES SPC700 RAM's "S" space). - SUPPORT.md: fix two stale "Rust 1.86" references (current pin is 1.96, per rust-toolchain.toml) flagged near an edited line. Verified NOT a bug, so not changed: the reviewer-flagged concern that `register_memory_maps`'s local `Vec<retro_memory_descriptor>` could be a use-after-free once dropped. Checked against RetroArch's actual runloop.c handling of RETRO_ENVIRONMENT_SET_MEMORY_MAPS: it calloc's its own descriptor array and copies each entry by value (`sys_info->mmaps.descriptors[i].core = mmaps->descriptors[i]`) before returning, so the frontend never retains a pointer into our transient Vec. Only the `ptr` field *within* each descriptor (into rustynes-core's heap-owned WRAM/SRAM/VRAM buffers) needs to outlive the call, and it already does -- the same buffers the pre-existing get_memory_data path has relied on since before this PR.
Summary
Closes #311. Adds the
.gitlab-ci.ymlthe libretro/RetroArch teamrequested to get RustyNES onto RetroArch's built-in core downloader
(the core was already integrated with the legacy
libretro-superscripts), and closes out the concrete libretro-core feature gaps
found while researching it.
Buildbot recipe (
crates/rustynes-libretro).gitlab-ci.yml: Windows x64, Linux x64, macOS x64/arm64, Android(4 ABIs), iOS arm64, tvOS arm64 via the shared
libretro-infrastructure/ci-templatesincludes — mirrors the twoproven, currently-building reference cores (
doukutsu-rs-libretro,holani-retro). Deliberately excludes webOS (no reference rust coreuses it, and the crate's own Makefile has no
webosbranch) andNintendo Switch/libnx (no
rust-libnx*.ymltemplate exists inci-templatesat all — confirmed, not assumed) as unproven for afirst submission, per the issue's "only include platforms that will
build successfully".
cargo build --release --target <triple>with no-pflag, thenmv lib${CORENAME}.<ext> ${CORENAME}_libretro.<ext>. Two fixes were required for this toactually work:
[lib] name = "rustynes"in the crate'sCargo.toml(previously unset, defaulting to
rustynes_libretrofrom thepackage name) — without this,
CORENAME=rustynes_libretrowoulddouble the
_libretrosuffix in the final artifact name, andCORENAME=rustyneswould look for a source file that doesn'texist. The crate's own
Makefileis updated to match.[workspace] default-members = ["crates/rustynes-libretro"]in the root
Cargo.toml, so the templates' unscopedcargo buildbuilds only this crate instead of all 18 workspace members (in
particular the frontend's wgpu/winit/cpal/wayland stack, which has
no business on a headless cross-compile buildbot image). Confirmed
safe: every CI workflow, this crate's Makefile, and every
scripts/*.shinvocation already scopes with-p/--workspace/--manifest-path. Fixed the 4 doc snippetsthat showed a bare
cargo buildso copy-pasting them still buildsthe whole workspace/frontend as intended.
osxbranch previously set no
RUST_TARGETat all) with explicitx86_64-apple-darwin/aarch64-apple-darwintriples keyed offARCH, falling back to native compile whenARCHis unset(unchanged local
makebehavior on macOS).Core feature completion (
crates/rustynes-libretro/src/lib.rs)RETRO_ENVIRONMENT_SET_MEMORY_MAPS: registers WRAM/SRAM/VRAMdescriptors — the memory-descriptor path RetroAchievements' rcheevos
prefers over the legacy pointer API (kept unchanged alongside it).
.fdscontent was previously always routedthrough
Emu::from_rom(cartridge-only), so FDS loading silentlyfailed despite
valid_extensionsadvertising it. Now detected viathe extension and routed to
Nes::from_diskwith adisksys.romlookup in the frontend's system directory.
Quick Menu, backed by the existing
Nes::disk_side_count/inserted_disk_side/set_disk_sideAPI (the same one the desktopfrontend's F9 keybind uses).
on_cheat_set/on_cheat_reset),backed by the already-existing
Nes::add_genie_codefamily(excluded from serialized state — no determinism impact).
get_fastforwarding-gated audio skip during RetroArch'sfast-forward/rollback-netplay catch-up. A modest, honest win —
rustynes-corehas no mixer-bypass API, so APU synthesis itselfisn't skipped.
option (
rustynes-corecaches region into fixed dividers once atconstruction, with no post-hoc setter — real core-construction
surgery, out of scope here) and Zapper/Power Pad peripheral wiring
(confirmed feasible via existing
Nes::set_zapper/set_power_pad,but needs substantially more new surface than this change's scope).
None of the feature-completion items touch CPU/PPU/APU emulation logic
or
on_serialize/on_unserialize— all are additive FFI/presentationwiring over already-existing, already-tested
rustynes-coreAPIs, soAccuracyCoin/golden-vector determinism is unaffected by design.
docs/libretro/advanced_features.mdupdated to describe what'sactually implemented, including a correction to an earlier draft of
this doc that mischaracterized RetroArch's own address-poke cheat
manager as unsupported (it never calls
on_cheat_setat all — itpokes the same exposed memory RetroAchievements uses, which has
worked since that API was first implemented).
Companion upstream PRs
Per
docs/libretro/UPSTREAM_SYNC.md's re-fork SOP:docs/library/rustynes.mdand 4 other"Adding a new core" checklist docs up to date.
rustynes_libretro.info'sstale
display_versionanddisk_control/mapper-count fields.Test plan
cargo build --release -p rustynes-libretroand a barecargo build --releasefrom the repo root both succeed,producing
target/release/librustynes.so(confirms thedefault-members+[lib] namefixes work end-to-end).crates/rustynes-libretro/Makefile'smake releasestillproduces the correctly-named
rustynes_libretro.so.cargo fmt --all --checkclean.cargo clippy --workspace --all-targets -- -D warningsclean.cargo clippy -p rustynes-frontend --all-targets --features scripting[,hd-pack]and--features retroachievementsclean.RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depsclean.
cargo test --workspace— 1905 passed, 13 ignored (116 suites).install for hands-on testing.