neser 1.2.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
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
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
use serde::{Deserialize, Serialize};

/// Action requested by a write to $FF55 (HDMA5 control register).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HdmaAction {
    /// Start a general-purpose DMA (bit 7 = 0, no active HDMA).
    StartGdma,
    /// Start an HBlank DMA (bit 7 = 1).
    StartHdma,
    /// Cancel an active HBlank DMA (bit 7 = 0 while HDMA is active).
    CancelHdma,
}

/// CGB VRAM DMA (HDMA) state for registers $FF51$FF55.
///
/// Supports two transfer modes:
/// - **GDMA** (General-Purpose DMA): transfers all blocks at once, CPU halted.
/// - **HDMA** (HBlank DMA): transfers one 16-byte block per HBlank (LY 0–143).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HdmaState {
    /// Source address (full 16-bit, lower 4 bits forced to 0).
    source: u16,
    /// Destination offset within VRAM ($0000–$1FF0, lower 4 bits forced to 0).
    destination: u16,
    /// Remaining block count (0-based: 0 = 1 block of 16 bytes, 0x7F = 128 blocks).
    remaining_blocks: u8,
    /// Whether a transfer is currently active (blocks being transferred).
    active: bool,
    /// `true` for HBlank DMA mode, `false` for GDMA mode.
    hblank_mode: bool,
    /// `true` when HDMA has been requested but not yet activated.
    /// Set when bit 7=1 is written to FF55; cleared when transfer starts or is cancelled.
    hdma_on_hblank: bool,
    /// `true` until the first write to HDMA5 ($FF55).
    /// Per Mooneye boot_hwio-C: HDMA5 reads $FF when no transfer has ever been started.
    #[serde(default = "default_never_started")]
    never_started: bool,
}

fn default_never_started() -> bool {
    true
}

impl Default for HdmaState {
    fn default() -> Self {
        Self::new()
    }
}

impl HdmaState {
    /// Create a new HDMA state with all registers cleared and no active transfer.
    pub fn new() -> Self {
        Self {
            source: 0,
            destination: 0,
            remaining_blocks: 0,
            active: false,
            hblank_mode: false,
            hdma_on_hblank: false,
            never_started: true,
        }
    }

    /// Write the high byte of the source address ($FF51 — HDMA1).
    pub fn write_source_high(&mut self, val: u8) {
        self.source = (self.source & 0x00F0) | (u16::from(val) << 8);
    }

    /// Write the low byte of the source address ($FF52 — HDMA2).
    /// Lower 4 bits are ignored (forced to 0).
    pub fn write_source_low(&mut self, val: u8) {
        self.source = (self.source & 0xFF00) | u16::from(val & 0xF0);
    }

    /// Write the high byte of the destination address ($FF53 — HDMA3).
    /// Only bits 4–0 of the high byte matter (destination is $8000–$9FF0).
    pub fn write_dest_high(&mut self, val: u8) {
        self.destination = (self.destination & 0x00F0) | (u16::from(val & 0x1F) << 8);
    }

    /// Write the low byte of the destination address ($FF54 — HDMA4).
    /// Lower 4 bits are ignored (forced to 0).
    pub fn write_dest_low(&mut self, val: u8) {
        self.destination = (self.destination & 0xFF00) | u16::from(val & 0xF0);
    }

    /// Write to the control register ($FF55 — HDMA5) and return the requested action.
    ///
    /// - Bit 7 = 0, no active HDMA → `StartGdma` (length = lower 7 bits)
    /// - Bit 7 = 1 → `StartHdma` (length = lower 7 bits, marks as requested)
    /// - Bit 7 = 0, active HDMA → `CancelHdma`
    pub fn write_control(&mut self, val: u8) -> HdmaAction {
        // Clear the never_started flag on first write.
        self.never_started = false;

        let length = val & 0x7F;
        let start_hblank = val & 0x80 != 0;

        if self.active && self.hblank_mode && !start_hblank {
            // Writing bit 7 = 0 while HDMA is active cancels the transfer.
            // Update remaining_blocks from the written value (even though cancelling).
            self.remaining_blocks = length;
            self.active = false;
            self.hdma_on_hblank = false;
            return HdmaAction::CancelHdma;
        }

        self.remaining_blocks = length;

        if start_hblank {
            self.hblank_mode = true;
            // HDMA is requested but not immediately activated.
            // The bus will activate it when LCD state is appropriate.
            self.hdma_on_hblank = true;
            HdmaAction::StartHdma
        } else {
            self.hblank_mode = false;
            self.hdma_on_hblank = false;
            // GDMA starts immediately.
            self.active = true;
            HdmaAction::StartGdma
        }
    }

    /// Read the control register ($FF55 — HDMA5).
    ///
    /// Returns remaining blocks in bits 0–6, and bit 7 = 1 when **inactive** (0 when active).
    /// Returns $FF when no transfer has ever been started (per Mooneye boot_hwio-C).
    pub fn read_control(&self) -> u8 {
        if self.never_started {
            0xFF
        } else if self.active {
            // Bit 7 = 0 (active), lower 7 bits = remaining blocks.
            self.remaining_blocks & 0x7F
        } else {
            // Bit 7 = 1 (not active). Per Pan Docs: after cancellation,
            // bit 7 is set but lower 7 bits still contain remaining blocks count.
            // After GDMA completion, returns $FF (all transfers complete).
            0x80 | (self.remaining_blocks & 0x7F)
        }
    }

    /// Transfer one 16-byte block using the provided read/write closures.
    ///
    /// Reads 16 bytes from `source`, writes to `destination` (VRAM offset),
    /// advances both addresses by 16, and decrements `remaining_blocks`.
    /// Returns `true` if the transfer is now complete (no more blocks remaining).
    pub fn transfer_block(
        &mut self,
        read_fn: &mut dyn FnMut(u16) -> u8,
        write_fn: &mut dyn FnMut(u16, u8),
    ) -> bool {
        for i in 0u16..16 {
            let byte = read_fn(self.source.wrapping_add(i));
            write_fn(self.destination.wrapping_add(i), byte);
        }
        self.source = self.source.wrapping_add(16);
        self.destination = self.destination.wrapping_add(16);

        if self.remaining_blocks == 0 {
            self.active = false;
            self.hdma_on_hblank = false;
            true
        } else {
            self.remaining_blocks -= 1;
            false
        }
    }

    /// Returns `true` if a transfer (GDMA or HDMA) is active.
    pub fn is_active(&self) -> bool {
        self.active
    }

    /// Returns `true` if HDMA has been requested but not yet activated.
    pub fn is_hdma_pending(&self) -> bool {
        self.hdma_on_hblank && !self.active
    }

    /// Activate a pending HDMA transfer.
    /// Should be called when conditions are right (LCD on, appropriate PPU state).
    pub fn activate_hdma(&mut self) {
        if self.hdma_on_hblank {
            self.active = true;
        }
    }

    /// Clear the pending HDMA flag without deactivating.
    /// Used when HDMA starts immediately in HBlank mode.
    pub fn clear_hblank_pending(&mut self) {
        self.hdma_on_hblank = false;
    }

    /// Advance source/destination addresses and decrement remaining blocks
    /// after one 16-byte block has been transferred externally.
    ///
    /// Used by `CgbBus` which handles the actual memory reads/writes directly
    /// (to avoid borrow-checker issues with closures over `&mut self`).
    /// Returns `true` if the transfer is now complete.
    pub fn advance_after_block(&mut self) -> bool {
        self.source = self.source.wrapping_add(16);
        self.destination = self.destination.wrapping_add(16);

        if self.remaining_blocks == 0 {
            self.active = false;
            self.hdma_on_hblank = false;
            true
        } else {
            self.remaining_blocks -= 1;
            false
        }
    }

    /// Returns `true` if the active transfer is HBlank DMA.
    pub fn is_hblank_mode(&self) -> bool {
        self.hblank_mode
    }

    /// Returns the current source address.
    pub fn source(&self) -> u16 {
        self.source
    }

    /// Returns the current VRAM destination offset.
    pub fn destination(&self) -> u16 {
        self.destination
    }

    /// Returns the number of remaining blocks (0-based).
    pub fn remaining_blocks(&self) -> u8 {
        self.remaining_blocks
    }

    /// Force the transfer to complete immediately (used when destination overflows).
    /// Sets active to false and clears remaining blocks.
    pub fn force_complete(&mut self) {
        self.active = false;
        self.hdma_on_hblank = false;
        self.remaining_blocks = 0;
    }

    /// Advance source/destination by a partial block (when overflow stops transfer mid-block).
    /// Advances addresses by `bytes_written`, then marks transfer complete.
    pub fn advance_by_partial_block(&mut self, bytes_written: u16) {
        self.source = self.source.wrapping_add(bytes_written);
        self.destination = self.destination.wrapping_add(bytes_written);
        self.active = false;
        self.hdma_on_hblank = false;
        self.remaining_blocks = 0;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // ── Source register tests ────────────────────────────────────────────────

    #[test]
    fn test_write_source_high_sets_upper_byte() {
        // Given: fresh HdmaState
        let mut hdma = HdmaState::new();
        // When: write $C0 to HDMA1 (source high)
        hdma.write_source_high(0xC0);
        // Then: source address upper byte is $C0
        assert_eq!(hdma.source() & 0xFF00, 0xC000);
    }

    #[test]
    fn test_write_source_low_masks_lower_4_bits() {
        // Given: fresh HdmaState
        let mut hdma = HdmaState::new();
        // When: write $3F to HDMA2 (source low)
        hdma.write_source_low(0x3F);
        // Then: lower 4 bits are forced to 0, so source low byte is $30
        assert_eq!(hdma.source() & 0x00FF, 0x0030);
    }

    #[test]
    fn test_write_source_preserves_other_byte() {
        // Given: HdmaState with source high set to $C0
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0xC0);
        // When: write $50 to source low
        hdma.write_source_low(0x50);
        // Then: full source is $C050
        assert_eq!(hdma.source(), 0xC050);
    }

    // ── Destination register tests ──────────────────────────────────────────

    #[test]
    fn test_write_dest_high_masks_upper_3_bits() {
        // Given: fresh HdmaState
        let mut hdma = HdmaState::new();
        // When: write $9F to HDMA3 (dest high)
        hdma.write_dest_high(0x9F);
        // Then: upper 3 bits ignored, only bits 4–0 preserved → $1F
        assert_eq!(hdma.destination() >> 8, 0x1F);
    }

    #[test]
    fn test_write_dest_low_masks_lower_4_bits() {
        // Given: fresh HdmaState
        let mut hdma = HdmaState::new();
        // When: write $AB to HDMA4 (dest low)
        hdma.write_dest_low(0xAB);
        // Then: lower 4 bits forced to 0 → $A0
        assert_eq!(hdma.destination() & 0x00FF, 0x00A0);
    }

    #[test]
    fn test_write_dest_preserves_other_byte() {
        // Given: HdmaState with dest high set to $80
        let mut hdma = HdmaState::new();
        hdma.write_dest_high(0x80);
        // When: write $F0 to dest low
        hdma.write_dest_low(0xF0);
        // Then: full destination is $00F0 (high bits masked: $80 & $1F = $00)
        assert_eq!(hdma.destination(), 0x00F0);
    }

    // ── Control register write tests ────────────────────────────────────────

    #[test]
    fn test_write_control_bit7_clear_starts_gdma() {
        // Given: no active transfer
        let mut hdma = HdmaState::new();
        // When: write $0F to HDMA5 (bit 7=0, length=0x0F → 16 blocks)
        let action = hdma.write_control(0x0F);
        // Then: GDMA started
        assert_eq!(action, HdmaAction::StartGdma);
        assert!(hdma.is_active());
        assert!(!hdma.is_hblank_mode());
        assert_eq!(hdma.remaining_blocks(), 0x0F);
    }

    #[test]
    fn test_write_control_bit7_set_starts_hdma() {
        // Given: no active transfer
        let mut hdma = HdmaState::new();
        // When: write $83 to HDMA5 (bit 7=1, length=$03 → 4 blocks)
        let action = hdma.write_control(0x83);
        // Then: HDMA is requested (pending), not immediately active
        // The bus will activate it when PPU conditions are right.
        assert_eq!(action, HdmaAction::StartHdma);
        assert!(hdma.is_hdma_pending());
        assert!(!hdma.is_active()); // Pending, not active
        assert!(hdma.is_hblank_mode());
        assert_eq!(hdma.remaining_blocks(), 0x03);
        // When bus activates the HDMA:
        hdma.activate_hdma();
        assert!(hdma.is_active());
        assert!(!hdma.is_hdma_pending());
    }

    #[test]
    fn test_write_control_bit7_clear_during_active_hdma_cancels() {
        // Given: active HDMA transfer
        let mut hdma = HdmaState::new();
        hdma.write_control(0x83); // Request HDMA
        hdma.activate_hdma(); // Bus activates it
        // When: write $00 to HDMA5 (bit 7=0 while HDMA active)
        let action = hdma.write_control(0x00);
        // Then: HDMA cancelled
        assert_eq!(action, HdmaAction::CancelHdma);
        assert!(!hdma.is_active());
    }

    // ── Control register read tests ─────────────────────────────────────────

    #[test]
    fn test_read_control_when_never_started_returns_ff() {
        // Given: no transfer ever started (per Mooneye boot_hwio-C)
        let hdma = HdmaState::new();
        // Then: $FF (never_started flag is set)
        assert_eq!(hdma.read_control(), 0xFF);
    }

    #[test]
    fn test_read_control_when_inactive_after_start_returns_80() {
        // Given: a transfer was started and completed
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0x80);
        hdma.write_dest_high(0x00);
        // Start a 1-block GDMA (length = 0 means 1 block)
        hdma.write_control(0x00);
        // Complete the transfer
        hdma.transfer_block(&mut |_| 0u8, &mut |_, _| {});
        // Then: $80 (inactive after transfer, 0 remaining)
        assert_eq!(hdma.read_control(), 0x80);
    }

    #[test]
    fn test_read_control_during_active_hdma_returns_remaining() {
        // Given: active HDMA with 4 blocks remaining (remaining_blocks = 3)
        let mut hdma = HdmaState::new();
        hdma.write_control(0x83); // Request HDMA, length=$03
        hdma.activate_hdma(); // Bus activates it
        // Then: bit 7 = 0 (active), lower 7 bits = 3
        assert_eq!(hdma.read_control(), 0x03);
    }

    #[test]
    fn test_read_control_after_gdma_completion_returns_ff() {
        // Given: GDMA with 1 block (length=0)
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0xC0);
        hdma.write_source_low(0x00);
        hdma.write_dest_high(0x80);
        hdma.write_dest_low(0x00);
        hdma.write_control(0x00); // GDMA, 1 block
        // When: transfer the single block
        let dummy_mem = [0u8; 0x10000];
        let complete =
            hdma.transfer_block(&mut |addr| dummy_mem[addr as usize], &mut |_addr, _val| {});
        // Then: transfer complete, read returns $80 (inactive + 0 remaining)
        // Note: remaining_blocks stays at 0 after completion, so we get $80 not $FF
        assert!(complete);
        assert_eq!(hdma.read_control(), 0x80);
    }

    // ── Transfer block tests ────────────────────────────────────────────────

    #[test]
    fn test_transfer_block_copies_16_bytes() {
        // Given: source at $C000, destination at $0000 (VRAM offset)
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0xC0);
        hdma.write_source_low(0x00);
        hdma.write_dest_high(0x80);
        hdma.write_dest_low(0x00);
        hdma.write_control(0x00); // GDMA, 1 block

        let mut src_mem = [0u8; 0x10000];
        for i in 0..16u8 {
            src_mem[0xC000 + i as usize] = i + 1;
        }
        let mut dest_writes: Vec<(u16, u8)> = Vec::new();

        // When: transfer one block
        hdma.transfer_block(&mut |addr| src_mem[addr as usize], &mut |addr, val| {
            dest_writes.push((addr, val))
        });

        // Then: 16 bytes written to VRAM offsets $0000–$000F
        assert_eq!(dest_writes.len(), 16);
        for i in 0..16u16 {
            assert_eq!(dest_writes[i as usize], (i, (i + 1) as u8));
        }
    }

    #[test]
    fn test_transfer_block_advances_addresses() {
        // Given: source at $C000, dest at $0000, 2 blocks
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0xC0);
        hdma.write_source_low(0x00);
        hdma.write_dest_high(0x80);
        hdma.write_dest_low(0x00);
        hdma.write_control(0x01); // GDMA, 2 blocks (remaining=1)

        let dummy_mem = [0u8; 0x10000];
        // When: transfer first block
        let complete =
            hdma.transfer_block(&mut |addr| dummy_mem[addr as usize], &mut |_addr, _val| {});
        // Then: not complete, addresses advanced by 16
        assert!(!complete);
        assert_eq!(hdma.source(), 0xC010);
        assert_eq!(hdma.destination(), 0x0010);
        assert_eq!(hdma.remaining_blocks(), 0x00);

        // When: transfer second block
        let complete =
            hdma.transfer_block(&mut |addr| dummy_mem[addr as usize], &mut |_addr, _val| {});
        // Then: complete
        assert!(complete);
    }

    #[test]
    fn test_transfer_block_returns_true_on_last_block() {
        // Given: GDMA with 1 block
        let mut hdma = HdmaState::new();
        hdma.write_source_high(0xC0);
        hdma.write_source_low(0x00);
        hdma.write_dest_high(0x80);
        hdma.write_dest_low(0x00);
        hdma.write_control(0x00); // 1 block (remaining=0)

        let dummy_mem = [0u8; 0x10000];
        let complete =
            hdma.transfer_block(&mut |addr| dummy_mem[addr as usize], &mut |_addr, _val| {});
        assert!(complete);
        assert!(!hdma.is_active());
    }
}