cge_nes 0.1.1

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Main NES console emulation functionality.
//!
//! This module provides the core NES console emulation through the [`NesConsole`] type,
//! which coordinates all the components of the system including the CPU, PPU, cartridge,
//! and input devices.

use crate::cartridge::Cartridge;
use crate::input::{HostInput, InputRegisters};
use crate::nes::dma::{Dma, DmaState, DmaType};
use crate::nes::ppu_cart_memory::PpuCartMemory;
use crate::nes::system::nes_memory_space::NesMemorySpace;
use crate::ppu::{Color, FrameEvent, Ppu};
use cpu6502::{Cpu, MemorySpace};
use devices6502::size_const::*;
use std::ops::DerefMut;
#[cfg(feature = "instr_log")]
use std::path::Path;

//const NES_CYCLES_SECOND: u64 = 1789773;
const PPU_CYCLES_PER_CPU_CYCLE: u8 = 3;

/// Main NES console emulator.
///
/// The `NesConsole` type represents a complete NES system, managing:
/// - CPU (MOS 6502)
/// - PPU (Picture Processing Unit)
/// - Internal RAM (2KB mirrored 4 times)
/// - Cartridge interface
/// - Input handling
/// - DMA (Direct Memory Access) transfers
/// To Do:
/// - Implement APU (Audio Processing Unit) emulation
#[derive(Default)]
pub struct NesConsole {
    cpu: Cpu,
    ppu: Ppu,
    internal_ram: devices6502::Mirror<devices6502::Ram<SIZE_2K>, 4>,
    cartridge: Option<Box<dyn Cartridge>>,
    input_registers: InputRegisters,
    ppu_dma: Dma,
    current_dma_put: DmaType,
    cycles_since_reset: usize,
    on: bool,
}

impl NesConsole {
    /// Creates a new NES console in its default powered-off state.
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns a slice of the current screen buffer colors.
    pub fn screen_colors(&self) -> &[Color] {
        self.ppu.screen_colors()
    }

    /// Inserts a cartridge into the console.
    ///
    /// # Panics
    ///
    /// Panics if a cartridge is already inserted.
    pub fn insert_cartridge(&mut self, cart: Box<dyn Cartridge>) {
        assert!(
            !self.cartridge_connected(),
            "attempt to insert cartridge when there is already one"
        );

        self.ppu.cycle_accurate_sprites_enabled = cart.requires_cycle_accurate_sprites();
        self.cartridge = Some(cart);
    }

    /// Returns whether the console is powered on.
    pub fn on(&self) -> bool {
        self.on
    }

    /// Removes the currently inserted cartridge.
    ///
    /// # Panics
    ///
    /// Panics if the console is powered on.
    pub fn remove_cartridge(&mut self) {
        assert!(!self.on);
        self.cartridge = None;
    }

    /// Returns whether a cartridge is currently inserted.
    pub fn cartridge_connected(&self) -> bool {
        self.cartridge.is_some()
    }

    /// Powers on the console, resetting all components to their initial state.
    pub fn switch_on(&mut self) {
        if self.on == false {
            self.cpu.reset();
            self.ppu.reset();
            self.cycles_since_reset = 0;
            self.on = true;

            #[cfg(feature = "instr_log")]
            {
                self.cpu.init_logging_debug(Path::new("nes_cpu_log.txt"));
            }
        }
    }

    /// Powers off the console.
    pub fn switch_off(&mut self) {
        self.on = false;
    }

    /// Resets the console while keeping it powered on.
    ///
    /// This simulates pressing the reset button on the console.
    pub fn reset(&mut self) {
        self.ppu.reset();
        self.cpu.reset();
        self.cycles_since_reset = 0;
    }

    /// Executes a single console cycle, including CPU, PPU, and DMA operations.
    ///
    /// # Arguments
    ///
    /// * `cartridge` - The currently inserted game cartridge providing memory mapping
    /// * `host_input` - Interface to read input device states from the host system
    ///
    /// # Returns
    ///
    /// Returns a [`FrameEvent`] indicating the current frame rendering state:
    /// - `None` - Normal cycle, no special events
    /// - `ReadyToPresent` - Frame is ready to be displayed
    /// - `EndOfFrame` - End of frame reached
    #[inline]
    pub fn run_console_cycle(
        &mut self,
        cartridge: &mut dyn Cartridge,
        host_input: &mut impl HostInput,
    ) -> FrameEvent {
        // Check if DMA needs to start after CPU instruction completion
        if self.ppu_dma.state() == DmaState::WaitingForCpuHalt {
            if self.cpu.is_current_cycle_write() == false {
                self.ppu_dma.start_transfer();
            }
        }

        // Handle DMA transfer cycles
        if self.ppu_dma.state() == DmaState::Transferring {
            if self.current_dma_put != DmaType::Ppu {
                // DMA read cycle - pull data from memory
                let pull_addr = self.ppu_dma.addr();
                let mut memory = NesMemorySpace::new(
                    &mut self.internal_ram,
                    cartridge,
                    &mut self.ppu,
                    &mut self.input_registers,
                    host_input,
                    &mut self.ppu_dma,
                );
                let data = memory.read(pull_addr);
                self.ppu_dma.data_pull(data);
            } else if let Some(data) = self.ppu_dma.data_put() {
                // DMA write cycle - write data to PPU OAM
                let mut ppu_memory = PpuCartMemory::new(cartridge);
                self.ppu
                    .write_ppu_register(data, crate::ppu::Register::OamData, &mut ppu_memory);
            }
        } else {
            // Normal CPU execution cycle
            let mut memory = NesMemorySpace::new(
                &mut self.internal_ram,
                cartridge,
                &mut self.ppu,
                &mut self.input_registers,
                host_input,
                &mut self.ppu_dma,
            );
            self.cpu.run(&mut memory);
            let _chr_rom_changed = memory.chr_rom_changed();
        };

        // Run PPU cycles (3 per CPU cycle)
        let mut ppu_memory = PpuCartMemory::new(cartridge);
        let mut nmi_signal = false;
        let mut frame_event = crate::ppu::FrameEvent::None;
        for _ in 0..PPU_CYCLES_PER_CPU_CYCLE {
            let new_frame_state = self.ppu.run_cycle(&mut ppu_memory);
            frame_event.join(new_frame_state);
            nmi_signal |= self.ppu.nmi_signal();
        }
        self.cpu.set_nmi_pin_value(nmi_signal);

        self.cpu.set_irq_pin_value(cartridge.irq_pin());

        // Toggle DMA read/write phase
        self.current_dma_put.toggle();

        self.cycles_since_reset += 1;

        frame_event
    }

    /// Runs the console until the PPU indicates it's ready to present a frame.
    ///
    /// This method will do nothing if the console is powered off or no cartridge
    /// is inserted.
    pub fn run_until_present(&mut self, host_input: &mut impl HostInput) {
        if self.on == false {
            return;
        }

        let mut cart = self.cartridge.take();

        if let Some(cart) = cart.as_mut() {
            let cart = cart.deref_mut();
            while self.run_console_cycle(cart, host_input) != FrameEvent::ReadyToPresent {}
        }

        self.cartridge = cart.take();
    }

    /// Runs the console for one complete frame.
    ///
    /// This method will do nothing if the console is powered off or no cartridge
    /// is inserted.
    pub fn run_frame(&mut self, host_input: &mut impl HostInput) {
        if self.on == false {
            return;
        }

        let mut cart = self.cartridge.take();

        if let Some(cart) = cart.as_mut() {
            let cart = cart.deref_mut();
            while self.run_console_cycle(cart, host_input) != FrameEvent::EndOfFrame {}
        }

        self.cartridge = cart.take();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cartridge::ChrRomContentStatus;
    use crate::input::{ConnectedSocket, InputDeviceState};

    struct MockCart;

    impl Cartridge for MockCart {
        fn read_cpu_mapped(&self, _addr: u16) -> u8 {
            0xEA
        }

        fn write_cpu_mapped(&mut self, _data: u8, _addr: u16) -> ChrRomContentStatus {
            ChrRomContentStatus::Unchanged
        }

        fn read_ppu_mapped(&mut self, _addr: u16) -> u8 {
            0
        }

        fn write_ppu_mapped(&mut self, _data: u8, _addr: u16) -> ChrRomContentStatus {
            ChrRomContentStatus::Unchanged
        }
    }

    /// Cartridge that opts into cycle-accurate sprite reads (e.g. MMC3-style
    /// mappers that use sprite CHR reads to drive a scanline counter).
    struct CycleAccurateSpriteCart;

    impl Cartridge for CycleAccurateSpriteCart {
        fn read_cpu_mapped(&self, _addr: u16) -> u8 {
            0xEA
        }

        fn write_cpu_mapped(&mut self, _data: u8, _addr: u16) -> ChrRomContentStatus {
            ChrRomContentStatus::Unchanged
        }

        fn read_ppu_mapped(&mut self, _addr: u16) -> u8 {
            0
        }

        fn write_ppu_mapped(&mut self, _data: u8, _addr: u16) -> ChrRomContentStatus {
            ChrRomContentStatus::Unchanged
        }

        fn requires_cycle_accurate_sprites(&self) -> bool {
            true
        }
    }

    struct MockHostInput;

    impl HostInput for MockHostInput {
        fn input_device_state(&mut self, _player: ConnectedSocket) -> InputDeviceState {
            Default::default()
        }
    }

    #[test]
    fn create_console_and_run_until_present() {
        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let mut host_input = MockHostInput;

        console.insert_cartridge(cart);
        console.switch_on();
        console.run_until_present(&mut host_input);

        let pixels_drawn = console.ppu.screen_colors().len();
        assert_eq!(pixels_drawn, 256 * 240);
    }

    const PPU_CYCLES_PER_SCANLINE: usize = 341;
    const PPU_SCANLINES: usize = 262;

    #[test]
    fn create_console_and_run_frames() {
        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let mut host_input = MockHostInput;

        console.insert_cartridge(cart);
        console.switch_on();
        console.run_frame(&mut host_input);
        console.run_frame(&mut host_input);

        let pixels_drawn = console.ppu.screen_colors().len();
        assert_eq!(pixels_drawn, 256 * 240);
        let ppu_cycles = PPU_CYCLES_PER_SCANLINE * PPU_SCANLINES * 2 - 1;
        assert_eq!(console.cycles_since_reset, ppu_cycles / 3);
    }

    #[test]
    #[cfg(not(debug_assertions))]
    fn create_console_and_run60sec() {
        use crate::ppu::Register;

        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let mut ppu_cart = Box::new(MockCart);
        let mut host_input = MockHostInput;
        let mut ppu_mem = PpuCartMemory::new(ppu_cart.as_mut());

        console.insert_cartridge(cart);
        console.switch_on();
        console
            .ppu
            .write_ppu_register(0b_00011110, Register::PpuMask, &mut ppu_mem);
        for _ in 0..3600 {
            console.run_frame(&mut host_input);
        }
    }

    #[test]
    fn run_without_cart() {
        let mut console = NesConsole::default();
        let mut host_input = MockHostInput;

        console.switch_on();
        console.run_frame(&mut host_input);
        assert_eq!(console.cycles_since_reset, 0);
    }

    #[test]
    #[should_panic(expected = "attempt to insert cartridge when there is already one")]
    fn connectwice_panics() {
        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let cart2 = Box::new(MockCart);

        console.insert_cartridge(cart);
        console.insert_cartridge(cart2);
    }

    #[test]
    #[should_panic(expected = "assertion failed: !self.on")]
    fn removewhileon_panics() {
        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let mut host_input = MockHostInput;

        console.insert_cartridge(cart);
        console.switch_on();
        console.run_frame(&mut host_input);
        console.remove_cartridge();
    }

    #[test]
    fn run_while_off() {
        let mut console = NesConsole::default();
        let cart = Box::new(MockCart);
        let mut host_input = MockHostInput;

        console.insert_cartridge(cart);
        console.run_frame(&mut host_input);
        assert_eq!(console.cycles_since_reset, 0);
    }

    #[test]
    fn insert_cartridge_propagates_cycle_accurate_sprites_flag() {
        // Default cart (MockCart) does not opt in, so the flag stays false.
        let mut console = NesConsole::default();
        console.insert_cartridge(Box::new(MockCart));
        assert!(!console.ppu.cycle_accurate_sprites_enabled);

        // A cart that opts in (e.g. MMC3) must turn the flag on so the PPU
        // runs the cycle-accurate sprite fetch path.
        let mut console = NesConsole::default();
        console.insert_cartridge(Box::new(CycleAccurateSpriteCart));
        assert!(console.ppu.cycle_accurate_sprites_enabled);
    }
}