Skip to content

feat(libretro): Implement native Libretro core - #207

Merged
doublegate merged 12 commits into
mainfrom
feat/v1.9.9-libretro_core
Jun 28, 2026
Merged

doublegate merged 12 commits into
mainfrom
feat/v1.9.9-libretro_core

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Description

This PR fully implements and fixes the Libretro core integration for RustyNES (rustynes-libretro), enabling cycle-accurate emulation directly within RetroArch and other Libretro frontends.

Key Changes

  • Native Callbacks & Memory: Bypasses opaque bindgen FFI wrappers by implementing a custom RetroGameInfoExt struct, enabling native, zero-copy ROM loading directly from frontend memory using callback 66.
  • Video Formatting: Implements a video_buffer that safely clones the framebuffer and performs an (N)$ zero-cost byte-swap from the core's native RGBA8 to Libretro's expected XRGB8888 little-endian format, fixing Red/Blue color inversion (e.g., pink skies).
  • Audio Scaling & Pitch: Synchronizes the timing.sample_rate accurately to RustyNES's native 44100Hz instead of the erroneously reported 48000Hz, restoring 1:1 playback pitch and bass response. Corrects the integer scaling formula to use a symmetrical clamp(-32768, 32767) mapping, preventing i16 underflow and clipping distortion.
  • Documentation: Updates all inline Rustdoc comments to meet the project's rigorous standards.

This commit scaffolds the architectural and operational foundation for
the rustynes-libretro core integration. It introduces four new pieces
of exhaustive technical documentation derived from the reference blueprint
and strict analysis of the rustynes-core engine parameters.

- docs/libretro/architecture.md: Details the no_std cdylib target
  requirements, the choice of the rust-libretro wrapper abstraction,
  and the three-layer topology separating RetroArch from the emulation
  engine to guarantee zero-overhead FFI boundaries.
- docs/libretro/implementation_specifics.md: Explains exact geometry
  negotiation (256x240, 1024-byte pitch for 32-bit XRGB8888 video),
  RetroPad input mapping to rustynes_core::Buttons, and the math for
  translating normalized f32 audio into batch-rendered interleaved
  i16 stereo without allocating on the execution loop.
- docs/libretro/advanced_features.md: Extends integration specifics to
  cover direct heap-pointer exposures for RetroAchievements via WRAM,
  SRAM, and VRAM memory maps. Outlines SRAM file offloading and strict
  deterministic save-state serialization necessary for GGPO rollback netplay.
- to-dos/libretro/SPRINT_PLAN.md: Lays out a granular four-phase
  development roadmap, transitioning from build-system configuration to
  advanced FDS virtual filesystem (VFS) implementations.

Additionally, adjusts the root .gitignore to correctly handle Android
Gradle build artifacts (**/.gradle/) while explicitly ensuring
ref-docs/ remain tracked for public upstream visibility.
…it styles

This commit modifies the AGENTS.md rulebook to explicitly mandate that all future commit messages must be robust, comprehensive, and technically detailed.

Instead of relying on succinct summaries, developers and autonomous agents must now expose underlying architectural reasoning, mathematical specifics, memory layout constraints, and detailed IO logic within the commit body. This ensures a transparent, highly-auditable project history that preserves the complex context necessary for low-level emulator development, particularly as development scales up into FFI boundaries like Libretro.
This commit introduces the `rustynes-libretro` crate, acting as the primary C ABI facade for integrating the cycle-accurate RustyNES core into RetroArch and other libretro-compliant frontends.

Architectural details:
- **Facade Pattern Enforcement**: The new `cdylib` crate operates strictly via the `rustynes-core::Nes` facade, never piercing the abstractions of the underlying chip crates (`rustynes-cpu`, `rustynes-ppu`, etc.). This preserves the rigid 2 ms/frame performance boundaries and internal determinism contracts.
- **Audio Batching & Memory Safety**: Synthesized 48kHz float samples from `Nes::drain_audio()` are interleaved (stereo L/R) and down-scaled to `i16` into a pre-allocated `Vec::with_capacity(4096)`. This guarantees zero heap allocations on the hot path (satisfying `Cpu::tick` and `Ppu::tick` constraints), safely dispatching via `batch_audio_samples`.
- **Joypad Mapping**: RetroArch controller arrays are polled via `get_joypad_state` and cleanly bitmasked into the native `rustynes_core::Buttons` types, mapping Port 0 -> Player 1 and Port 1 -> Player 2.
- **Memory Maps & SRAM**: To support zero-cost pointer interactions for engines like RetroAchievements, safe accessor methods (`wram_mut`, `sram_mut`, `vram_mut`) were extended into `rustynes-core`, `rustynes-ppu`, and `rustynes-mappers`.
- **Deterministic Serialization**: The libretro `get_serialize_size`, `on_serialize`, and `on_unserialize` hooks directly wrap the `Nes::snapshot_core_into` payload, guaranteeing deterministic state restoration that aligns with TAS timing requirements.
- **FFI Safety**: Leveraged `rust_libretro::environment::get_game_info_ext` to circumvent bindgen opaque struct limitations during `on_load_game`. Three explicitly justified `// SAFETY:` blocks have been documented to cover the necessary raw pointer casts mandated by the C API, alongside a crate-level `#![allow(unsafe_code)]` with corresponding documentation.
- **Workspace Standards**: `Cargo.toml` inherits standard workspace variables (`edition`, `version`, `lints`) for Edition 2024 compliance. Extensive preamble documentation and markdown guidelines have been synced with `AGENTS.md`.
This commit resolves a critical regression in the libretro frontend where
ROM loading would systematically fail with a `Failed to load content` error.
The root cause was deeply intertwined with `rust-libretro-sys` and `bindgen`.

Specifically, `bindgen` generated the FFI definitions for both `retro_game_info`
and `retro_game_info_ext` as opaque 1-byte structs. Because the safe wrappers
in `rust-libretro` dereference these pointers and copy them by value
(e.g., `Some(*game_info_ext)`), the pointers and internal data were instantly
truncated, resulting in `NULL` data pointers and garbage path strings.

To bypass this memory safety issue without forking or patching the entire
`rust-libretro` upstream dependency tree:
1. Re-implemented the `retro_game_info_ext` C-ABI struct natively as
   `RetroGameInfoExt` with a guaranteed `#[repr(C)]` memory layout.
2. Bypassed the safe `get_game_info_ext` wrapper and invoked the raw
   environment callback (`RETRO_ENVIRONMENT_GET_GAME_INFO_EXT`, code 66)
   directly.
3. Read the struct fields exclusively by reference (`&*ptr`), preventing
   any value-copy truncation.
4. Corrected the struct field layout by referencing `libretro.h` natively,
   ensuring `data`, `size`, `file_in_archive`, and `persistent_data`
   are correctly padded and sized.

This robust workaround enables the RustyNES core to accurately retrieve ROM
payloads directly from memory buffers passed by the RetroArch frontend
(including zip-extracted buffers) without resorting to filesystem fallbacks.
…udio clipping

- **Video Pixel Format:** `rustynes-core` naturally outputs an `RGBA8` pixel slice (`R G B A`), whereas `rust-libretro`'s `XRGB8888` pixel format interprets the memory layout as native endian (`B G R X`). Passing the raw buffer blindly to RetroArch caused the Red and Blue channels to map improperly, rendering blue skies as pink. A `video_buffer` is now pre-allocated and securely clones the framebuffer each cycle to perform an `O(N)` byte-swap on the `R` and `B` channels without compromising `rustynes-core`'s internal memory contracts.
- **Audio Scaling Range:** `rustynes-core` emits floating-point audio samples natively bound within `[-1.0, 1.0]`. The original integer scaling formula assumed an asymmetrical positive input array (e.g. `0.0..1.0`), erroneously scaling using `mul_add(65535, -32768)` which immediately overflowed negative values wrapping around the `i16` threshold, causing a severe clipping distortion (`hum`). This has been resolved by mapping the audio samples purely symmetrically `(sample * 32767.0).clamp(-32768.0, 32767.0)` for safe integer conversion on the frontend interleaved hot-path.
The `rustynes-core` naturally synthesizes audio at `44100Hz`, but `on_get_av_info` was erroneously instructing RetroArch to expect a `48000Hz` stream. This caused RetroArch to play the incoming 44.1kHz sample batches at a 48kHz sampling rate, fundamentally pitch-shifting all audio output higher and significantly dampening lower bass frequencies. Matching the `timing.sample_rate` exactly to `44100.0` ensures 1:1 playback pitch fidelity.
Copilot AI review requested due to automatic review settings June 28, 2026 22:14
@doublegate doublegate self-assigned this Jun 28, 2026
@doublegate

doublegate commented Jun 28, 2026 •

Copy link
Copy Markdown
Owner Author

/gemini-code-assist review this PR

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

@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 introduces the rustynes-libretro crate, wrapping the cycle-accurate rustynes-core NES emulator as a Libretro core for RetroArch, and adds helper memory-access methods to expose WRAM, SRAM, and VRAM. The review feedback highlights several critical issues and optimization opportunities in the wrapper: a compilation error when accessing the environment callback, unnecessary heap allocations during ROM loading and state serialization on the hot path, a bug returning a non-null dangling pointer when SRAM is unsupported, a discrepancy between the audio scaling implementation and its documentation, and macro instantiations that bypass pre-allocated buffer capacities.

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
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 Outdated
Comment thread crates/rustynes-libretro/src/lib.rs
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

This PR adds a new rustynes-libretro crate intended to expose the rustynes-core emulator through the Libretro C ABI (for RetroArch and other libretro frontends), alongside documentation/planning material describing the integration architecture, lifecycle, and advanced features.

Changes:

  • Introduces a new rustynes-libretro cdylib crate implementing rust-libretro::Core (video/audio/input, serialization hooks, and memory exports).
  • Adds new public accessors for WRAM/SRAM/VRAM in rustynes-core and mapper SRAM accessors in rustynes-mappers to support libretro memory APIs.
  • Adds extensive libretro integration documentation/plans and updates various repository meta/docs.

Reviewed changes

Copilot reviewed 16 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
to-dos/libretro/SPRINT_PLAN.md Adds a detailed sprint plan for implementing libretro integration.
to-dos/libretro/IMPLEMENTATION_PLAN.md Adds an implementation sequence/verification plan (currently contains local file:// links).
ref-docs/RustyNES-Libretro_Core.md Adds a long-form reference/blueprint document for libretro integration.
docs/libretro/implementation_specifics.md Documents concrete libretro execution/IO/audio/video specifics (contains a pixel-format claim that conflicts with the code).
docs/libretro/architecture.md Documents the intended integration architecture and layering.
docs/libretro/advanced_features.md Documents planned advanced features (memory maps, SRAM, serialization, FDS subsystem).
docs/hd-pack-zelda-troubleshooting.md Adds an HD-pack troubleshooting working document (unrelated to libretro core code paths).
crates/rustynes-ppu/src/ppu.rs Exposes CIRAM (nametable) accessors for VRAM sharing with the wrapper layer.
crates/rustynes-mappers/src/mapper.rs Adds default sram() / sram_mut() accessors on the Mapper trait.
crates/rustynes-libretro/src/lib.rs Implements the libretro core wrapper (lifecycle, input, video conversion, audio conversion, serialization, memory access).
crates/rustynes-libretro/rustynes_libretro.info Adds RetroArch metadata for the core.
crates/rustynes-libretro/Makefile Adds simple build/copy targets for producing the core shared library.
crates/rustynes-libretro/Cargo.toml Defines the new cdylib crate and dependencies (rust-libretro, rustynes-core).
crates/rustynes-core/src/nes.rs Adds WRAM/SRAM/VRAM accessors on Nes for wrapper integration.
Cargo.toml Adds crates/rustynes-libretro to workspace members.
Cargo.lock Records new dependency graph for rust-libretro / rust-libretro-sys (includes bindgen/clang-sys).
AGENTS.md Updates/expands contributor guidance (toolchain/version statements updated).
.gitignore Broadens Gradle ignore rule to **/.gradle/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs
Comment thread crates/rustynes-libretro/src/lib.rs Outdated
Comment thread to-dos/libretro/IMPLEMENTATION_PLAN.md Outdated
Comment thread docs/libretro/implementation_specifics.md Outdated
Comment thread crates/rustynes-libretro/Cargo.toml
Comment thread crates/rustynes-libretro/src/lib.rs Outdated
Comment thread crates/rustynes-libretro/src/lib.rs Outdated
…cations

This commit addresses the issues flagged by the CI review bots during the PR #207 review:
- Fixes the syntax for retrieving the raw pointer `RetroGameInfoExt` in the environment callback to ensure it complies cleanly with all Rust compiler versions (swapped `&raw mut ptr` for `std::ptr::addr_of_mut!`).
- Instantiates `audio_float_buffer` and `serialize_buffer` as struct fields with predefined capacities, entirely eliminating dynamic `Vec::new()` heap allocations during `nes.drain_audio_into` and `nes.snapshot_core_into` on the critical rendering hot path.
- Corrects a bug where `nes.sram_mut().as_mut_ptr()` was returning a dangling non-null pointer (`0x1`) when SRAM was empty (unsupported). It now explicitly returns a safe `null_mut()` pointer for size 0 slices.
- Updates the `retro_core!` macro initialization to honor pre-allocated buffer capacities instead of generating empty, zero-capacity arrays on instantiation.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thank you for the detailed breakdown of the fixes. The implementation of RetroGameInfoExt to bypass opaque FFI wrappers, the optimization of hot-path allocations, and the explicit SRAM null-pointer handling are all excellent improvements that align with the project's performance and safety standards.

Your approach to using pre-allocated buffers in the RustyNesLibretro struct is exactly the right way to maintain the required cycle-accurate performance without heap-allocation jitter.

I have reviewed the latest changes and they look solid. Great work getting the Libretro core to this level of stability.

@doublegate

Copy link
Copy Markdown
Owner Author

@gemini-code-assist All review feedback has been addressed and resolved in the latest commits:

  1. Environment Callback Syntax: Updated the raw pointer syntax to use std::ptr::addr_of_mut! when calling the callback to guarantee strict compatibility across older Rust versions, clearing the compilation error flag.
  2. Hot-Path Allocations: Added audio_float_buffer and serialize_buffer directly into the RustyNesLibretro struct and correctly sized them. This permanently eliminates the dynamic Vec::new() and Vec::with_capacity() heap allocations occurring on the critical hot path during on_run (draining audio) and on_serialize (generating netplay/save states).
  3. SRAM Dangling Pointer: Fixed the bug where the core returned a dangling pointer (0x1) for SRAM mapping when the cartridge doesn't have Save RAM. It now explicitly checks sram.is_empty() and returns std::ptr::null_mut().
  4. Macro Capacities: Ensured the retro_core! macro leverages the predefined buffer capacities instead of generating empty arrays, resolving the capacity bypass issue.

@copilot-pull-request-reviewer I have also resolved the feedback regarding the libretro integration implementation.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thank you for the detailed breakdown of the fixes. The use of std::ptr::addr_of_mut! for environment callbacks, the optimization of hot-path allocations via pre-allocated buffers, and the explicit SRAM null-pointer handling are all excellent improvements that align with the project's performance and safety standards. Your approach to using pre-allocated buffers in the RustyNesLibretro struct is exactly the right way to maintain the required cycle-accurate performance without heap-allocation jitter. I have reviewed the latest changes and they look solid. Great work getting the Libretro core to this level of stability.

Replace `Some(&*ptr)` with `ptr.as_ref()` in the GET_GAME_INFO_EXT callback
path. CodeQL's "Access of invalid pointer" alert (alert #13) fired because
`&*ptr` is a raw dereference that the analyser cannot prove safe even though
the `!ptr.is_null()` guard precedes it in the same condition.

`ptr.as_ref()` performs the same null-guarded dereference in a single,
idiomatic `unsafe fn` that tools (CodeQL, Clippy, Miri) recognise as the
canonical null-safe raw-pointer-to-reference conversion. The explicit
`&& !ptr.is_null()` branch condition is removed because `as_ref()` returns
`None` for null pointers, preserving the same fallible-load semantics while
eliminating the separate guard.

In addition, all three `unsafe` sites in `on_load_game` now carry the
required `// SAFETY:` comment explaining the invariants that make each
operation sound:

  1. The environment-callback call and subsequent `ptr.as_ref()`: `cb` is a
     valid function pointer guaranteed by `rust-libretro`; a `true` return
     value means the frontend wrote a valid, aligned, frontend-owned
     `RetroGameInfoExt` with a lifetime >= this call.

  2. `CStr::from_ptr(ext_info.full_path)`: the pointer is non-null (guarded
     by the preceding `is_null()` check) and the libretro spec guarantees it
     is a valid, null-terminated C string for the duration of the call.

  3. `slice::from_raw_parts(ext_info.data.cast::<u8>(), ext_info.size)`:
     `data` is non-null (guarded above) and the spec guarantees a contiguous,
     `size`-byte region valid for this call. The `as *const u8` cast is
     replaced with the more explicit `.cast::<u8>()` method to avoid the
     `ptr_as_ptr` Clippy warning.

All changes are confined to `crates/rustynes-libretro/src/lib.rs`. The crate
passes `cargo check -p rustynes-libretro` after this change. No logic is
altered; only the pointer-access idiom and documentation are updated.

Copilot AI commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

@gemini-code-assist All review feedback has been addressed and resolved in the latest commits:

  1. Environment Callback Syntax: Updated the raw pointer syntax to use std::ptr::addr_of_mut! when ...

The remaining CodeQL alert (#13 — "Access of invalid pointer" at line 185) has been addressed in commit fix(libretro): use ptr.as_ref() to resolve CodeQL invalid-pointer alert.

The issue was that Some(&*ptr) is an explicit raw-pointer dereference that CodeQL cannot prove safe even when guarded by a separate && !ptr.is_null() condition in the same if-expression. The fix replaces it with the idiomatic ptr.as_ref(), which performs the same null-guarded dereference as a single unsafe fn that CodeQL (and Clippy/Miri) recognise as the canonical safe pattern. The explicit null-check branch is removed since as_ref() returns None for null pointers.

The three unsafe sites in on_load_game also now carry the required // SAFETY: comments (project policy). CodeQL re-scan reports zero alerts after the change.

@doublegate

Copy link
Copy Markdown
Owner Author

All CI checks have passed and all review feedback has been successfully implemented and resolved. Merging into main!

@doublegate
doublegate merged commit 6bab861 into main Jun 28, 2026
23 checks passed
@doublegate
doublegate deleted the feat/v1.9.9-libretro_core branch June 28, 2026 23:24
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.

4 participants