Add modal controls dialog (header bar button) with per-player keyboard
remapping, key capture mode, duplicate protection, reset to defaults,
and a gamepad tab stub for future Xbox/DualSense support.
- Replace Cairo DrawingArea with custom NesScreen widget using
GskTextureScaleNode for GPU-accelerated nearest-neighbor rendering
- Migrate from FileChooserNative to FileDialog (GTK 4.10+)
- Add AlertDialog for error display, structured logging via env_logger
- Add FPS counter (F3), NTSC/PAL toggle (F7), fullscreen (F11),
Esc to quit, save/load state (Ctrl+S/L), volume slider
- Add player 2 keyboard input support
- Fix window proportions by compensating for header bar height
Remove FrameExecutor indirection by inlining its logic into
RuntimeHostLoop. Add set_video_mode() for NTSC/PAL switching,
set_joypad2_buttons() for player 2 input, and fix mapper scanline
IRQ test timing.
- Add ExRAM (modes 0-1) and fill-mode nametable routing via
read_nametable_byte / write_nametable_byte mapper hooks
- Separate sprite and BG CHR bank sets ($5120-$5127 vs $5128-$512B);
BG banks are only active in 8x16 sprite mode
- Use mapper.ppu_read_sprite() for sprite tile loads so they always
use the sprite bank set regardless of PPU fetch phase
- Replace CPU-cycle IRQ stub with scanline-based counter matching
Mesen2 hardware behaviour: fire when counter == irq_scanline at
dot 2 (start of scanline), irq_scanline=0 never fires
- Add Mapper::notify_frame_start() called unconditionally at the PPU
frame boundary; MMC5 uses it to hard-reset the scanline counter even
when rendering is disabled (e.g. during room transitions), preventing
stale counter values from shifting the CHR split by 8+ scanlines
- Fix CHR bank calculation for modes 0-2: use << 3/2/1 shifts instead
of & !7/3/1 masking to correctly convert bank numbers to 1KB indices
- Correct $5204 read: bit 7 = IRQ pending (cleared on read), bit 6 =
in-frame flag; IRQ line stays asserted until $5204 is read
- Dispatch $4020-$5FFF CPU reads/writes to mapper cpu_read_low /
cpu_write_low so MMC5 internal registers are accessible
#3 audio.rs: replace linear interpolation with Catmull-Rom Hermite cubic.
Stores prev_sample as p0 control point; m1=(p2-p0)/2, m2=(p2-p1)/2
tangents give continuous first derivative across batch boundaries.
#4 ppu: add per-slot sprite shift registers (spr_shift_lo/hi, spr_x_counter,
spr_attr_latch). load_sprite_shifters fetches pattern bytes with h-flip at
dot 1 of each visible scanline. sprite_pixel_from_shifters replaces the
per-pixel OAM scan; sprite-0 hit detection integrated into the shifter path.
#5 joypad.rs: format_controller_read now preserves bits 1-5,7 as open bus
(!0x41 mask) instead of zeroing bits 1-4, matching NES hardware behaviour.
Three audio bugs fixed:
1. DMC loop mode skipped the last byte of each sample iteration.
provide_dmc_dma_byte() was immediately setting dmc_dma_request on
loop restart while the sample buffer was still full, causing the
while-loop in clock_cpu_cycles to service a second DMA immediately
and overwrite the valid buffer. Per NES hardware spec, the reader
only fills an empty buffer — the request is now left to clock_dmc
when the output unit actually empties the buffer into the shift
register. Fixes intermittent clicking/crackling in games that use
looped DMC samples (BGM, SFX).
2. Missing DC blocker (high-pass filter) in AudioMixer. The NES APU
has a capacitor-coupled output stage that blocks DC bias. Without
it, abrupt channel state changes (length counter expiry, sweep
mute, triangle period < 2) produce DC steps that manifest as
audible clicks. Added a one-pole IIR high-pass filter at ~5 Hz
applied after the existing low-pass filter.
3. cpal stream was opened at application startup with
BufferSize::Fixed(256), forcing PipeWire/PulseAudio to run the
entire audio graph at a 5.3 ms quantum. This disrupted other audio
applications (browsers, media players) even when no ROM was loaded.
Fixed by: (a) creating the stream lazily on the first push_samples
call so no device is touched until a ROM is running, and (b)
switching to BufferSize::Default so the audio server chooses the
quantum instead of the emulator imposing one. Ring buffer capacity
increased from 1536 to 4096 samples to absorb larger server quanta.
- Fix frame counter running at 2× speed: clock_frame_counter now skips
odd CPU cycles (APU cycle = CPU/2), so envelope, sweep, and length
counters tick at the correct rate. Fixes sweep-driven whistle in Megaman II.
- Switch audio sampling to per-CPU-cycle granularity in
run_until_frame_complete_with_audio to eliminate square-wave harmonic
aliasing caused by sampling only once per instruction.
- Add IIR one-pole low-pass filter (~14 kHz) to AudioMixer to smooth
abrupt level transitions (crackling) introduced by correct envelope timing.
- Mute triangle channel when timer_period < 2 (≥27 kHz), which aliases
into the audible range at 48 kHz. Real NES RC circuit removes these
ultrasonics; emulator must suppress them explicitly.
- Update all APU bus tests to use correct (doubled) CPU cycle counts.
- CpalAudioSink writes to SPSC ring buffer, cpal callback reads
- Graceful fallback to silent operation on audio device errors
- Volume slider (0-100%) in header bar with speaker icon
- Ring buffer cleared on ROM load and reset