Files
yaze/docs/E4-Emulator-Development-Guide.md
scawful deb14c17a9 feat: Add Emulator Development Guide and Remove gRPC Integration Document
- Introduced a comprehensive Emulator Development Guide detailing the YAZE SNES emulator's architecture, usage, and debugging journey.
- Documented current emulator status, usage methods, architecture details, critical bug fixes, logging system, and future work.
- Removed the gRPC Integration Complete document as it is no longer relevant to the current documentation structure.
- Updated index to include the new Emulator Development Guide for better navigation.
2025-10-06 19:21:40 -04:00

7.1 KiB

E4 - Emulator Development Guide

Last Updated: October 6, 2025 Status: FUNCTIONAL & STABLE

This document provides a comprehensive overview of the YAZE SNES emulator subsystem, consolidating all development notes, bug fixes, and architectural decisions. It serves as the single source of truth for understanding and developing the emulator.

1. Current Status

The YAZE SNES emulator is fully functional and stable. All critical timing, synchronization, and instruction execution bugs in the APU/SPC700 and CPU have been resolved. The emulator can successfully boot and run "The Legend of Zelda: A Link to the Past" and other SNES games.

  • CPU-APU Synchronization: Cycle-accurate.
  • SPC700 Emulation: All instructions and timing are correct.
  • IPL ROM Protocol: Handshake and data transfers work as expected.
  • Memory System: Stable and consolidated.
  • UI Integration: Runs smoothly within the YAZE GUI.

The main remaining work involves UI/UX enhancements and adding advanced debugging features, rather than fixing core emulation bugs.

2. How to Use the Emulator

Method 1: Main Yaze Application (GUI)

  1. Build YAZE:
    cmake --build build --target yaze -j8
    
  2. Run YAZE:
    ./build/bin/yaze.app/Contents/MacOS/yaze
    
  3. Open a ROM: Use File > Open ROM or drag and drop a ROM file onto the window.
  4. Start Emulation:
    • Navigate to View > Emulator from the menu.
    • Click the Play (▶) button in the emulator toolbar.

Method 2: Standalone Emulator (yaze_emu)

For headless testing and debugging, use the standalone yaze_emu executable.

# Run for a specific number of frames and then exit
./build/bin/yaze_emu.app/Contents/MacOS/yaze_emu --emu_max_frames=600

# Run with a specific ROM
./build/bin/yaze_emu.app/Contents/MacOS/yaze_emu --emu_rom=path/to/rom.sfc

# Enable APU and CPU debug logging
./build/bin/yaze_emu.app/Contents/MacOS/yaze_emu --emu_debug_apu=true --emu_debug_cpu=true

3. Architecture

Memory System

The emulator's memory architecture was consolidated to resolve critical bugs and improve clarity.

  • rom_: A std::vector<uint8_t> that holds the cartridge ROM data. This is the source of truth for the emulator core's read path (cart_read()).
  • ram_: A std::vector<uint8_t> for SRAM.
  • memory_: A 16MB flat address space used only by the editor interface for direct memory inspection, not by the emulator core during execution.

This separation fixed a critical bug where the editor and emulator were reading from different, inconsistent memory sources.

CPU-APU-SPC700 Interaction

The SNES audio subsystem is complex and requires precise timing.

  1. Initialization: The SNES CPU boots and initializes the APU.
  2. IPL ROM Boot: The SPC700 (the APU's CPU) executes its 64-byte internal IPL ROM.
  3. Handshake: The SPC700 writes $AA and $BB to its output ports. The CPU reads these values and writes back $CC to initiate a data transfer.
  4. Data Transfer: The CPU uploads the audio driver and data to the SPC700's RAM in blocks. This involves a counter-based acknowledgment protocol.
  5. Execution: Once the audio driver is uploaded, the SPC700 jumps to the new code and begins handling audio processing independently.

4. The Debugging Journey: A Summary of Critical Fixes

The path to a functional emulator involved fixing a cascade of 9 critical, interconnected bugs.

  1. APU Cycle Synchronization: The APU was not advancing its cycles in sync with the master clock, causing an immediate deadlock.

    • Fix: Implemented a delta-based calculation in Apu::RunCycles() using g_last_master_cycles.
  2. SPC700 read_word Address Truncation: 16-bit addresses were being truncated to 8 bits, causing the SPC700 to read its reset vector from the wrong location ($00C0 instead of $FFC0).

    • Fix: Changed function parameters in spc700.h from uint8_t to uint16_t.
  3. Multi-Step Instruction bstep Increment: Instructions like MOVS were only executing their first step because the internal step counter (bstep) was never incremented.

    • Fix: Added bstep++ to the first step of all multi-step instructions.
  4. Step Reset Logic: The main instruction loop was resetting the step counter unconditionally, breaking multi-step instructions.

    • Fix: Guarded the step reset with if (bstep == 0).
  5. Opcode Re-Read: A new opcode was being fetched before the previous multi-step instruction had completed.

    • Fix: Guarded the opcode read with if (bstep == 0).
  6. Address Re-Calculation: Address mode functions were being called on each step of a multi-step instruction, advancing the PC incorrectly.

    • Fix: Cached the calculated address in this->adr on the first step and reused it.
  7. CMP Z-Flag Calculation: CMP instructions were checking the 16-bit result for zero, causing incorrect flag calculations for 8-bit operations.

    • Fix: Changed all CMP functions to check (result & 0xFF) == 0.
  8. IPL ROM Counter Write: The IPL ROM was missing a key instruction to echo the transfer counter back to the CPU.

    • Fix: Corrected the IPL ROM byte array in apu.cc to include CB F4 (MOV ($F4),Y).
  9. SDL Event Loop Blocking: The main application loop used SDL_WaitEvent, which blocked rendering unless the user moved the mouse.

    • Fix: Switched to SDL_PollEvent to enable continuous rendering at 60 FPS.

5. Logging System

A structured logging system (util/log.h) was integrated to replace all printf statements.

  • Categories: APU, SNES, CPU, Memory, SPC700.
  • Levels: DEBUG, INFO, WARN, ERROR.
  • Usage: LOG_INFO("APU", "Reset complete");

How to Enable

# Run with debug logging for specific categories
./build/bin/yaze --log-level=DEBUG --log-categories=APU,SNES

# Log to a file
./build/bin/yaze --log-level=DEBUG --log-file=emulator.log

6. Testing

The emulator subsystem has a growing suite of tests.

  • Unit Tests: Located in test/unit/emu/, these verify specific components like the APU handshake (apu_ipl_handshake_test.cc).
  • Standalone Emulator: yaze_emu provides a headless way to run the emulator for a fixed number of frames, perfect for regression testing.

Running Tests

# Build the test runner
cmake --build build --target yaze_test

# Run all emulator-related tests
./build/bin/yaze_test --gtest_filter="*Apu*":"*Spc700*"

7. Future Work & Enhancements

While the core is stable, future work can focus on:

  • UI Enhancements: Create a dedicated debugger panel in the UI with register views, memory editors, and breakpoints.
  • z3ed Integration: Expose emulator controls (start, stop, step, save state) to the z3ed CLI for automated testing and AI-driven debugging.
  • Audio Output: Connect the S-DSP's output buffer to SDL audio to enable sound.
  • Performance Optimization: Profile and optimize hot paths, potentially with a JIT compiler for the CPU.
  • Expanded Test Coverage: Add comprehensive tests for all CPU and PPU instructions.