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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
//! VRAM address helpers and masks used by the PPU.
//!
//! Provides constants and helper functions to compose/decompose the PPU's
//! 14-bit VRAM address into coarse/fine X/Y and nametable bits.
use crate::ppu::registers::Registers;

const VRAM_COARSE_X_MASK: u16 = 0b0001_1111;
const VRAM_COARSE_Y_MASK: u16 = 0b0011_1110_0000;
const VRAM_NAMETABLE_INDEX_X_MASK: u16 = 0b0000_0100_0000_0000;
const VRAM_NAMETABLE_INDEX_Y_MASK: u16 = 0b0000_1000_0000_0000;
const VRAM_FINE_Y_MASK: u16 = 0b0111_0000_0000_0000;
const VRAM_NAMETABLE_INDEX_MASK: u16 = VRAM_NAMETABLE_INDEX_X_MASK | VRAM_NAMETABLE_INDEX_Y_MASK;
const VRAM_VERTICAL_BITS_MASK: u16 =
    VRAM_FINE_Y_MASK | VRAM_NAMETABLE_INDEX_Y_MASK | VRAM_COARSE_Y_MASK;
const VRAM_HORIZONTAL_BITS_MASK: u16 = VRAM_COARSE_X_MASK | VRAM_NAMETABLE_INDEX_X_MASK;

const VRAM_COARSE_X_ATTRIBUTE_MASK: u16 = 0b0000_0000_0111;
const VRAM_COARSE_Y_ATTRIBUTE_MASK: u16 = 0b0000_0011_1000;
const VRAM_ATTRIBUTE_OFFSET: u16 = 0b0000_0011_1100_0000;
const VRAM_ATTRIBUTE_NAMETABLE_MASK: u16 = 0b0000_1100_0000_0000;

const VRAM_COARSE_X_SHIFT: u16 = 0;
const VRAM_COARSE_Y_SHIFT: u16 = 5;
const VRAM_NAMETABLE_INDEX_SHIFT: u16 = 10;
const VRAM_FINE_Y_SHIFT: u16 = 12;

const VRAM_ATTRIBUTE_COARSE_X_SHIFT: u16 = 0;
const VRAM_ATTRIBUTE_COARSE_Y_SHIFT: u16 = 3;
const VRAM_ATTRIBUTE_NAMETABLE_INDEX_SHIFT: u16 = 10;

const VRAM_NAMETABLE_BASE_ADDR: u16 = 0x2000;

#[derive(Default, Debug)]
/// Decomposed VRAM address components as used by the NES PPU scrolling logic.
///
/// The PPU internally treats the current VRAM address as separate fields:
/// - fine_y: fine vertical scroll within a tile (0..=7)
/// - coarse_x/coarse_y: tile indices inside the nametable (0..=31)
/// - nametable_index: selects one of the four nametables via two bits
pub struct VramAddressComponents {
    /// Fine Y scroll (bits 12..=14 of the VRAM address), range 0..=7.
    pub fine_y: u8,
    /// Nametable index bits (two bits selecting the nametable quadrant).
    pub nametable_index: u8,
    /// Coarse Y tile coordinate (bits 5..=9).
    pub coarse_y: u8,
    /// Coarse X tile coordinate (bits 0..=4).
    pub coarse_x: u8,
}

impl VramAddressComponents {
    /// Combine components back into a 15-bit VRAM address (without base offset).
    pub fn build_vram_address(&self) -> u16 {
        let fine_y = ((self.fine_y as u16) << VRAM_FINE_Y_SHIFT) & VRAM_FINE_Y_MASK;
        let nametable_index = ((self.nametable_index as u16) << VRAM_NAMETABLE_INDEX_SHIFT)
            & VRAM_NAMETABLE_INDEX_MASK;
        let coarse_y = ((self.coarse_y as u16) << VRAM_COARSE_Y_SHIFT) & VRAM_COARSE_Y_MASK;
        let coarse_x = ((self.coarse_x as u16) << VRAM_COARSE_X_SHIFT) & VRAM_COARSE_X_MASK;

        fine_y | nametable_index | coarse_y | coarse_x
    }

    /// Build the nametable address for fetching the tile index.
    pub fn build_nametable_address(&self) -> u16 {
        let addr = self.build_vram_address();
        (addr & !VRAM_FINE_Y_MASK) + VRAM_NAMETABLE_BASE_ADDR
    }

    /// Build the attribute-table address for the 2x2 tile quadrant.
    pub fn build_attribute_address(&self) -> u16 {
        let coarse_x = (self.coarse_x as u16) >> 2; // 3 bits for coarse X
        let coarse_x = (coarse_x << VRAM_ATTRIBUTE_COARSE_X_SHIFT) & VRAM_COARSE_X_ATTRIBUTE_MASK;
        let coarse_y = (self.coarse_y as u16) >> 2; // 3 bits for coarse Y
        let coarse_y = (coarse_y << VRAM_ATTRIBUTE_COARSE_Y_SHIFT) & VRAM_COARSE_Y_ATTRIBUTE_MASK;
        let nametable_index = self.nametable_index as u16;
        let nametable_index = (nametable_index << VRAM_ATTRIBUTE_NAMETABLE_INDEX_SHIFT)
            & VRAM_ATTRIBUTE_NAMETABLE_MASK;

        (nametable_index | VRAM_ATTRIBUTE_OFFSET | coarse_x | coarse_y) + VRAM_NAMETABLE_BASE_ADDR
    }

    /// Construct components from a raw VRAM address value.
    pub fn from_vram_address(addr: u16) -> Self {
        //let addr = addr & !VRAM_NAMETABLE_BASE_ADDR;
        let fine_y = ((addr & VRAM_FINE_Y_MASK) >> VRAM_FINE_Y_SHIFT) as u8;
        let nametable_index =
            ((addr & VRAM_NAMETABLE_INDEX_MASK) >> VRAM_NAMETABLE_INDEX_SHIFT) as u8;
        let coarse_y = ((addr & VRAM_COARSE_Y_MASK) >> VRAM_COARSE_Y_SHIFT) as u8;
        let coarse_x = (addr & VRAM_COARSE_X_MASK) as u8;

        Self {
            fine_y,
            nametable_index,
            coarse_y,
            coarse_x,
        }
    }
}

/// Convert a pixel coordinate to the coarse tile component (coord / 8).
fn coarse_component(coord: u8) -> u8 {
    coord >> 3
}

/// Extract the fine scroll component within a tile (coord % 8).
fn fine_component(coord: u8) -> u8 {
    coord & 0b0000_0111
}

/// Split an 8-bit pixel coordinate into coarse (tile) and fine (intra-tile) components.
///
/// Equivalent to `(coord / 8, coord % 8)` but uses bit ops to match PPU behaviour.
///
/// # Parameters
/// * `coord` - Pixel coordinate in the range 0..=255
///
/// # Returns
/// `(coarse, fine)` where `coarse` is 0..=31 and `fine` is 0..=7.
pub fn separate_coarse_fine_from_coord(coord: u8) -> (u8, u8) {
    (coarse_component(coord), fine_component(coord))
}

/// Merge coarse (tile) and fine (intra-tile) components into an 8-bit coordinate.
///
/// Equivalent to `coarse * 8 + fine` with masking to the valid bit widths.
///
/// # Parameters
/// * `coarse` - Tile index (lower 5 bits used)
/// * `fine` - Intra-tile offset (lower 3 bits used)
///
/// # Returns
/// Combined coordinate in the range 0..=255.
pub fn combine_coarse_fine_into_coord(coarse: u8, fine: u8) -> u8 {
    let coarse = coarse & 0b0001_1111;
    let fine = fine & 0b0000_0111;
    coarse << 3 | fine
}

impl Registers {
    /// Return the decomposed components of the current VRAM address (`v`).
    pub fn v_addr_components(&self) -> VramAddressComponents {
        VramAddressComponents::from_vram_address(self.v)
    }

    /// Return the decomposed components of the temporary VRAM address (`t`).
    pub fn t_addr_components(&self) -> VramAddressComponents {
        VramAddressComponents::from_vram_address(self.t)
    }

    /// Update vertical bits in the VRAM address from scroll Y and current nametable.
    pub fn vram_addr_update_vertical_bits(&mut self) {
        if self.rendering_enabled() {
            // NES PPU: during pre-render, dots 280–304, copy vertical bits from t to v
            // Copy fine Y, coarse Y, and nametable Y bit directly from t
            let vertical_from_t = self.t & VRAM_VERTICAL_BITS_MASK;
            let horizontal_from_v = self.v & !VRAM_VERTICAL_BITS_MASK;
            self.v = horizontal_from_v | vertical_from_t;
        }
    }

    /// Update horizontal bits in the VRAM address from scroll X and current nametable.
    pub fn vram_addr_update_horizontal_bits(&mut self) {
        if self.rendering_enabled() {
            // NES PPU: at dot 257 of visible scanlines, copy horizontal bits from t to v
            // Copy coarse X and nametable X directly
            let horizontal_from_t = self.t & VRAM_HORIZONTAL_BITS_MASK;
            let vertical_from_v = self.v & !VRAM_HORIZONTAL_BITS_MASK;
            self.v = vertical_from_v | horizontal_from_t;
        }
    }

    /// Increment the coarse X in VRAM address, toggling horizontal nametable on overflow.
    pub fn vram_addr_increment_horizontal(&mut self) {
        if self.rendering_enabled() {
            let old_addr = self.v;
            let mut addr_components = VramAddressComponents::from_vram_address(old_addr);

            // Increment the coarse X component
            // If it overflows, toggle the nametable index bit
            // and reset coarse X to 0
            addr_components.coarse_x += 1;
            if addr_components.coarse_x == 0b0010_0000 {
                addr_components.coarse_x = 0;
                addr_components.nametable_index ^= 0b0001; // Toggle the nametable index x bit
            }

            self.v = addr_components.build_vram_address();
        }
    }

    /// Increment fine Y in VRAM address, cascading to coarse Y and vertical nametable bits.
    pub fn vram_addr_increment_vertical(&mut self) {
        if self.rendering_enabled() {
            // NES PPU increment Y (Loopy's doc):
            // if fine Y < 7: fine Y++
            // else fine Y = 0; switch on coarse Y:
            //   if coarse Y == 29: coarse Y = 0; toggle nametable Y
            //   else if coarse Y == 31: coarse Y = 0 (no toggle)
            //   else: coarse Y++
            let old_addr = self.v;
            let mut addr_components = VramAddressComponents::from_vram_address(old_addr);

            if addr_components.fine_y < 7 {
                addr_components.fine_y += 1;
            } else {
                addr_components.fine_y = 0;
                match addr_components.coarse_y {
                    29 => {
                        addr_components.coarse_y = 0;
                        addr_components.nametable_index ^= 0b0010; // toggle Y nametable
                    }
                    31 => {
                        // wrap to 0 without toggling nametable (attribute fetch rows)
                        addr_components.coarse_y = 0;
                    }
                    _ => {
                        addr_components.coarse_y = addr_components.coarse_y.wrapping_add(1);
                    }
                }
            }

            self.v = addr_components.build_vram_address();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ppu::registers::ppu_mask::PpuMaskFlags;
    use crate::ppu::registers::Register;

    #[test]
    fn vram_address_components_from_tile_addr() {
        let addr = 0b0000_0000_0000_0000;
        let components = VramAddressComponents::from_vram_address(addr);
        assert_eq!(components.fine_y, 0);
        assert_eq!(components.nametable_index, 0);
        assert_eq!(components.coarse_y, 0);
        assert_eq!(components.coarse_x, 0);

        let addr = 0b0001_1111_0011_1100;
        let components = VramAddressComponents::from_vram_address(addr);
        assert_eq!(components.fine_y, 0b0000_0001);
        assert_eq!(components.nametable_index, 0b0000_0011);
        assert_eq!(components.coarse_y, 0b0001_1001);
        assert_eq!(components.coarse_x, 0b0001_1100);
    }

    #[test]
    fn vram_address_components_build_vram_address() {
        let components = VramAddressComponents {
            fine_y: 0b0000_0001,
            nametable_index: 0b0000_0011,
            coarse_y: 0b0001_1001,
            coarse_x: 0b0001_1100,
        };
        let addr = components.build_vram_address();
        assert_eq!(addr, 0b0001_1111_0011_1100);
    }

    #[test]
    fn vram_address_components_build_nametable_address() {
        let components = VramAddressComponents {
            fine_y: 0b0000_0001,
            nametable_index: 0b0000_0011,
            coarse_y: 0b0001_1001,
            coarse_x: 0b0001_1100,
        };
        let addr = components.build_nametable_address();
        assert_eq!(addr, 0b0010_1111_0011_1100);
    }

    #[test]
    fn vram_address_components_build_attribute_address() {
        let components = VramAddressComponents {
            fine_y: 0,
            nametable_index: 0b0000_0010,
            coarse_y: 0b0001_1001,
            coarse_x: 0b0001_1100,
        };
        let addr = components.build_attribute_address();
        assert_eq!(addr, 0b010_1011_1111_0111);
    }

    fn enable_rendering(regs: &mut Registers) {
        regs.ppu_mask.insert(PpuMaskFlags::SHOW_BG);
        // Push the updated SHOW_BG flag into the ring buffer enough times so index 0 becomes true
        for _ in 0..4 {
            regs.update_render_toggle_buffers();
        }
    }

    fn set_v_components(
        regs: &mut Registers,
        coarse_x: u8,
        coarse_y: u8,
        fine_y: u8,
        nt_index: u8,
    ) {
        regs.v = VramAddressComponents {
            fine_y: fine_y & 0b111,
            nametable_index: nt_index & 0b11,
            coarse_y: coarse_y & 0b1_1111,
            coarse_x: coarse_x & 0b1_1111,
        }
        .build_vram_address();
    }

    fn set_t_components(
        regs: &mut Registers,
        coarse_x: u8,
        coarse_y: u8,
        fine_y: u8,
        nt_index: u8,
    ) {
        regs.t = VramAddressComponents {
            fine_y: fine_y & 0b111,
            nametable_index: nt_index & 0b11,
            coarse_y: coarse_y & 0b1_1111,
            coarse_x: coarse_x & 0b1_1111,
        }
        .build_vram_address();
    }

    #[test]
    fn horizontal_bits_copy_from_t_when_rendering_enabled() {
        let mut regs = Registers::default();
        enable_rendering(&mut regs);

        // v has coarse_x=3, nametable_x=0; t has coarse_x=20, nametable_x=1
        set_v_components(&mut regs, 3, 7, 2, 0b10);
        set_t_components(&mut regs, 20, 7, 2, 0b11); // only X-related bits differ (coarse_x and nt X bit)

        let v_before = regs.v;
        regs.vram_addr_update_horizontal_bits();
        let v_after = regs.v;

        // vertical bits must remain from v_before; horizontal bits must match t
        let horiz_after = v_after & super::VRAM_HORIZONTAL_BITS_MASK;
        let horiz_from_t = regs.t & super::VRAM_HORIZONTAL_BITS_MASK;
        assert_eq!(
            horiz_after, horiz_from_t,
            "horizontal bits should copy from t at dot 257 when rendering"
        );

        let vert_after = v_after & super::VRAM_VERTICAL_BITS_MASK;
        let vert_from_v_before = v_before & super::VRAM_VERTICAL_BITS_MASK;
        assert_eq!(
            vert_after, vert_from_v_before,
            "vertical bits should be preserved when copying horizontal bits"
        );
    }

    #[test]
    fn vertical_bits_copy_from_t_when_rendering_enabled() {
        let mut regs = Registers::default();
        enable_rendering(&mut regs);

        // v has coarse_y=10, fine_y=1, nt_y=0; t has coarse_y=25, fine_y=6, nt_y=1
        set_v_components(&mut regs, 12, 10, 1, 0b00);
        set_t_components(&mut regs, 12, 25, 6, 0b10);

        let v_before = regs.v;
        regs.vram_addr_update_vertical_bits();
        let v_after = regs.v;

        // vertical bits must copy from t; horizontal preserved
        let vert_after = v_after & super::VRAM_VERTICAL_BITS_MASK;
        let vert_from_t = regs.t & super::VRAM_VERTICAL_BITS_MASK;
        assert_eq!(vert_after, vert_from_t);

        let horiz_after = v_after & super::VRAM_HORIZONTAL_BITS_MASK;
        let horiz_from_v_before = v_before & super::VRAM_HORIZONTAL_BITS_MASK;
        assert_eq!(horiz_after, horiz_from_v_before);
    }

    #[test]
    fn horizontal_increment_wraps_and_toggles_nametable_x() {
        let mut regs = Registers::default();
        enable_rendering(&mut regs);

        // Start with coarse_x = 31 (0b1_1111), nametable_x = 0
        set_v_components(&mut regs, 31, 5, 0, 0b00);
        let before = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(before.coarse_x, 31);
        assert_eq!(before.nametable_index & 0b01, 0);

        regs.vram_addr_increment_horizontal();
        let after = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(
            after.coarse_x, 0,
            "coarse_x should wrap to 0 when incrementing from 31"
        );
        assert_eq!(
            after.nametable_index & 0b01,
            1,
            "nametable X bit should toggle on coarse_x wrap"
        );
    }

    #[test]
    fn vertical_increment_matches_loopy_rules() {
        let mut regs = Registers::default();
        enable_rendering(&mut regs);

        // Case 1: fine_y < 7 just increments fine_y
        set_v_components(&mut regs, 0, 0, 3, 0);
        regs.vram_addr_increment_vertical();
        let c = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(c.fine_y, 4);
        assert_eq!(c.coarse_y, 0);

        // Case 2: fine_y == 7 and coarse_y in middle -> coarse_y++ and fine_y resets
        set_v_components(&mut regs, 0, 10, 7, 0);
        regs.vram_addr_increment_vertical();
        let c = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(c.fine_y, 0);
        assert_eq!(c.coarse_y, 11);

        // Case 3: fine_y == 7 and coarse_y == 29 -> coarse_y=0 and toggle nametable Y
        set_v_components(&mut regs, 0, 29, 7, 0b00);
        regs.vram_addr_increment_vertical();
        let c = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(c.fine_y, 0);
        assert_eq!(c.coarse_y, 0);
        assert_eq!(c.nametable_index & 0b10, 0b10);

        // Case 4: fine_y == 7 and coarse_y == 31 -> coarse_y=0 and no toggle
        set_v_components(&mut regs, 0, 31, 7, 0b10);
        regs.vram_addr_increment_vertical();
        let c = VramAddressComponents::from_vram_address(regs.v);
        assert_eq!(c.fine_y, 0);
        assert_eq!(c.coarse_y, 0);
        assert_eq!(c.nametable_index & 0b10, 0b10);
    }

    #[test]
    fn mid_scanline_scroll_changes_take_effect_on_horizontal_copy() {
        let mut regs = Registers::default();
        enable_rendering(&mut regs);

        // Initialize v to some horizontal position: coarse_x=5, nt_x=0
        set_v_components(&mut regs, 5, 12, 0, 0b00);

        // Emulate writing PPUSCROLL mid-scanline to change X scroll to a new value
        // First PPUSCROLL write sets coarse_x and fine x; choose value= (coarse=18, fine=3)
        // 18*8 + 3 = 147 -> 0b1001_0011
        regs.write_register(147, Register::PpuScroll);
        // Sanity: t should now have coarse_x=18 and fine_y unchanged; x should be 3
        let t_before_copy = regs.t_addr_components();
        assert_eq!(t_before_copy.coarse_x, 18);
        assert_eq!(regs.x & 0b111, 3);

        // Before the horizontal-copy moment (dot 257), v must remain unchanged
        let v_before_copy = regs.v_addr_components();
        assert_eq!(v_before_copy.coarse_x, 5);

        // Simulate dot 257 behaviour: copy horizontal bits from t to v
        regs.vram_addr_update_horizontal_bits();
        let v_after_copy = regs.v_addr_components();
        assert_eq!(
            v_after_copy.coarse_x, 18,
            "coarse_x in v should reflect the new scroll after the copy"
        );
        assert_eq!(
            v_after_copy.nametable_index & 0b01,
            t_before_copy.nametable_index & 0b01
        );
    }
}