Skip to content

feat(libretro): add buildbot CI recipe and complete core feature set#312

Merged
doublegate merged 3 commits into
mainfrom
feat/libretro-buildbot-ci
Jul 19, 2026
Merged

feat(libretro): add buildbot CI recipe and complete core feature set#312
doublegate merged 3 commits into
mainfrom
feat/libretro-buildbot-ci

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Summary

Closes #311. Adds the .gitlab-ci.yml the libretro/RetroArch team
requested to get RustyNES onto RetroArch's built-in core downloader
(the core was already integrated with the legacy libretro-super
scripts), 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-templates includes — mirrors the two
    proven, currently-building reference cores (doukutsu-rs-libretro,
    holani-retro). Deliberately excludes webOS (no reference rust core
    uses it, and the crate's own Makefile has no webos branch) and
    Nintendo Switch/libnx (no rust-libnx*.yml template exists in
    ci-templates at all
    — confirmed, not assumed) 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.
    • [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 doc snippets
      that showed a bare cargo build so copy-pasting them still builds
      the whole workspace/frontend as intended.
  • Also fixes the crate Makefile's macOS cross-target gap (the osx
    branch previously set no RUST_TARGET at all) with 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 — the memory-descriptor path RetroAchievements' rcheevos
    prefers over the legacy pointer API (kept unchanged alongside it).
  • 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 and routed to Nes::from_disk with a disksys.rom
    lookup in the frontend's system directory.
  • Disk-control interface for FDS multi-side swap in RetroArch's
    Quick Menu, 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).
  • Native Game Genie cheats (on_cheat_set/on_cheat_reset),
    backed by the already-existing Nes::add_genie_code family
    (excluded from serialized state — no determinism impact).
  • get_fastforwarding-gated audio skip during RetroArch's
    fast-forward/rollback-netplay catch-up. A modest, honest win —
    rustynes-core has no mixer-bypass API, so APU synthesis itself
    isn't skipped.
  • Investigated but explicitly not attempted: a region/timing core
    option (rustynes-core caches region into fixed dividers once at
    construction, 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/presentation
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, 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_set at all — it
pokes 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:

Test plan

  • cargo build --release -p rustynes-libretro and a bare
    cargo build --release from the repo root both succeed,
    producing target/release/librustynes.so (confirms the
    default-members + [lib] name fixes work end-to-end).
  • crates/rustynes-libretro/Makefile's make release still
    produces the correctly-named rustynes_libretro.so.
  • cargo fmt --all --check clean.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • cargo clippy -p rustynes-frontend --all-targets --features scripting[,hd-pack] and --features retroachievements clean.
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
    clean.
  • cargo test --workspace — 1905 passed, 13 ignored (116 suites).
  • Manually installed the freshly built core into a local RetroArch
    install for hands-on testing.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml using libretro’s shared CI templates and align produced artifact naming via [lib] name = "rustynes".
  • Scope unqualified workspace builds to crates/rustynes-libretro via [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.

Comment thread SUPPORT.md Outdated
Comment thread crates/rustynes-libretro/src/lib.rs Outdated
Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread docs/libretro/advanced_features.md Outdated
- 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.
@doublegate
doublegate merged commit b49dd1e into main Jul 19, 2026
23 checks passed
@doublegate
doublegate deleted the feat/libretro-buildbot-ci branch July 19, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] add recipe for libretro buildbot CI

2 participants