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
//! Mapper 037 - ZZ board MMC3 multicart (Super Mario Bros + Tetris + Nintendo World Cup)
//!
//! Specifications:
//! - Main: <https://www.nesdev.org/wiki/INES_Mapper_037>
//!
//! Known Limitations:
//! - No known gameplay-blocking functional limitations are currently documented.
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mmc3::MMC3Mapper;
use crate::nes::cartridge::{Mapper, MapperCapabilities};
/// Mapper 037 - ZZ board MMC3-based 3-in-1 multicart
///
/// Hardware: MMC3 inner mapper + outer 3-bit register at $6000-$7FFF
///
/// Specifications:
/// - Main: <https://www.nesdev.org/wiki/INES_Mapper_037>
/// - PRG-ROM: 256 KiB total (addressed via outer register + MMC3)
/// - CHR: 256 KiB total (upper half selected by Q2)
/// - No PRG-RAM ($6000-$7FFF is the outer block register only)
///
/// Outer register ($6000-$7FFF): [.... .Q2Q1Q0]
/// - Writable only when MMC3's PRG-RAM is enabled and write-protect is clear ($A001)
/// - Power-on state: Q = 0
///
/// PRG bank mapping (8KB banks):
/// - A16 = (Q1 & Q0) | (Q2 & M16) where M16 = bit 3 of MMC3's raw PRG bank
/// - A17 = Q2
/// - final_bank = (A17 << 4) | (A16 << 3) | (mmc3_bank & 0x07)
///
/// CHR 1KB bank mapping:
/// - final_bank = (Q2 << 7) | (mmc3_1k_bank & 0x7F)
///
/// Known games: Super Mario Bros, Tetris, Nintendo World Cup
pub struct Mapper37 {
pub(crate) inner: MMC3Mapper,
outer_reg: u8, // bits [2:0] = Q[2:0]
}
impl Mapper37 {
const MAPPER_NUMBER: u8 = 37;
const PRG_BANK_SIZE: usize = 0x2000; // 8 KiB (same as MMC3)
const PRG_BANK_MASK: usize = Self::PRG_BANK_SIZE - 1;
const CHR_1K_BANK_SIZE: usize = 0x0400; // 1 KiB (same as MMC3)
const CHR_BANK_MASK: usize = Self::CHR_1K_BANK_SIZE - 1;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let prg_rom = ctx.prg_rom;
let chr_rom = ctx.chr_rom;
let mirroring = ctx.mirroring;
Self {
inner: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
outer_reg: 0,
}
}
/// Adjust the MMC3's raw 8KB PRG bank using the outer register.
///
/// PRG A16 = (Q1 & Q0) | (Q2 & M16), where M16 is MMC3 PRG A16 (raw bit 3)
/// PRG A17 = Q2
fn adjust_prg_bank(&self, mmc3_bank: usize) -> usize {
let q = self.outer_reg as usize;
let q0 = q & 1;
let q1 = (q >> 1) & 1;
let q2 = (q >> 2) & 1;
let m16 = (mmc3_bank >> 3) & 1;
let a16 = (q1 & q0) | (q2 & m16);
let a17 = q2;
(a17 << 4) | (a16 << 3) | (mmc3_bank & 0x07)
}
/// Adjust the MMC3's raw 1KB CHR bank using the outer register.
///
/// CHR A17 = Q2
fn adjust_chr_bank(&self, mmc3_1kb_bank: usize) -> usize {
let q2 = (self.outer_reg as usize >> 2) & 1;
(q2 << 7) | (mmc3_1kb_bank & 0x7F)
}
}
impl Mapper for Mapper37 {
fn base(&self) -> &BaseMapper {
&self.inner.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.inner.base
}
fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
Some(&self.inner)
}
fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
Some(&mut self.inner)
}
fn read_prg(&self, addr: u16) -> u8 {
if !(0x8000..=0xFFFF).contains(&addr) {
return 0; // No PRG-RAM; $6000-$7FFF is the outer register only
}
let raw_bank = self.inner.mapped_prg_bank(addr);
let final_bank = self.adjust_prg_bank(raw_bank);
let offset = (addr as usize) & Self::PRG_BANK_MASK;
self.inner.read_prg_at_bank(final_bank, offset)
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (0x6000..=0x7FFF).contains(&addr) {
if self.inner.is_prg_ram_writable() {
self.outer_reg = value & 0x07;
}
} else {
self.inner.write_prg(addr, value);
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
let raw_bank = self.inner.mapped_chr_1k_bank(addr);
let final_bank = self.adjust_chr_bank(raw_bank);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.inner.read_chr_1k_at(final_bank, offset)
}
fn write_chr(&mut self, addr: u16, value: u8) {
let raw_bank = self.inner.mapped_chr_1k_bank(addr);
let final_bank = self.adjust_chr_bank(raw_bank);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.inner.write_chr_1k_at(final_bank, offset, value);
}
fn mapper_number(&self) -> u16 {
u16::from(Self::MAPPER_NUMBER)
}
fn wram_size(&self) -> usize {
0 // No PRG-RAM; $6000-$7FFF is the outer block register
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.inner.registers_snapshot();
snap.push(self.outer_reg);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if let Some((&outer_reg, mmc3_data)) = data.split_last() {
self.outer_reg = outer_reg & 0x07;
self.inner.restore_registers(mmc3_data);
}
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
has_expansion_audio: false,
max_prg_ram_kb: 0,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
// 32 PRG banks × 8 KiB = 256 KiB.
const PRG_BANKS: usize = 32;
// 256 CHR 1KB banks = 256 KiB (Q2=0 → banks 0-127, Q2=1 → banks 128-255)
const CHR_1K_BANKS: usize = 256;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(1024, CHR_1K_BANKS);
create_mapper(MapperContext::new_for_test(
37,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 37 should be implemented")
}
// --- Factory ---
#[test]
fn mapper_37_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
37,
banked_data(8 * 1024, PRG_BANKS),
banked_data(1024, CHR_1K_BANKS),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 37 must be registered in the factory"
);
}
// --- Power-on / PRG block 0 ---
/// At power-on Q=0, the visible PRG window is $00000-$0FFFF (banks 0-7).
/// The fixed-last slot therefore resolves to bank 7.
#[test]
fn power_on_outer_reg_is_0_prg_from_first_64kb() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
7,
"Power-on Q=0: fixed-last PRG must be bank 7 (first 64 KiB window)"
);
}
/// Q=0, MMC3 R6=3 → $8000 reads bank 3 (within outer_block 0)
#[test]
fn prg_banking_with_mmc3_registers_within_first_block() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0b0000_0110); // select MMC3 register R6
mapper.write_prg(0x8001, 3); // R6 = 3
// Q=0: final = (0<<4)|(3&0xF) = 3
assert_eq!(
mapper.read_prg(0x8000),
3,
"Q=0, R6=3: $8000 must read bank 3"
);
}
/// Q=3 (0b011) forces PRG A16 high while A17 stays low, selecting banks 8-15.
#[test]
fn outer_reg_q3_selects_second_64kb_prg_block() {
let mut mapper = make_mapper();
// Enable PRG-RAM write ($A001=0x80) so outer register is writable
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x03); // Q=3
assert_eq!(
mapper.read_prg(0xE000),
15,
"Q=3: fixed-last PRG must be bank 15 (second 64 KiB window)"
);
}
/// Q=4 (0b100): Q2=1, Q1=0, Q0=0.
/// This selects the $20000-$3FFFF 128 KiB PRG window; the fixed-last slot resolves to bank 31.
#[test]
fn outer_reg_q4_q2_set_selects_third_128kb_prg_block() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x04); // Q=4
assert_eq!(
mapper.read_prg(0xE000),
31,
"Q=4: fixed-last PRG must be bank 31 (third 128 KiB window)"
);
}
// --- CHR banking ---
/// Q=0 (Q2=0): CHR stays in first 128 KiB. R2=5 → final CHR = (0<<7)|(5&0x7F) = 5
/// Q=4 (Q2=1): CHR shifts to second 128 KiB. R2=5 → final CHR = (1<<7)|(5&0x7F) = 133
#[test]
fn chr_a17_follows_q2() {
let mut mapper = make_mapper();
// Q=0: CHR bank at $1000 (R2=5) should be bank 5
mapper.write_prg(0x8000, 0b0000_0010); // select R2
mapper.write_prg(0x8001, 5); // R2=5
assert_eq!(
mapper.read_chr(0x1000),
5,
"Q=0 (Q2=0): CHR R2=5 must map to bank 5 (first 128 KiB)"
);
// Switch to Q=4 (Q2=1): same R2=5 but shifted to second 128 KiB
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x04); // Q=4
assert_eq!(
mapper.read_chr(0x1000),
133,
"Q=4 (Q2=1): CHR R2=5 must map to bank 133 (second 128 KiB)"
);
}
// --- Outer register write protection ---
/// Outer register write at $6000 is only accepted when MMC3 PRG-RAM is enabled
/// and write-protect is clear ($A001 bit7=1, bit6=0).
#[test]
fn outer_reg_write_requires_prg_ram_write_enable() {
let mut mapper = make_mapper();
// At power-on PRG-RAM is writable; set Q=3 to move to block 1
mapper.write_prg(0x6000, 0x03); // Q=3 accepted (power-on state is writable)
assert_eq!(
mapper.read_prg(0xE000),
15,
"Q=3 accepted at power-on: fixed-last must be 15"
);
// Disable PRG-RAM write-enable ($A001=0xC0: enabled but write-protected)
mapper.write_prg(0xA001, 0xC0);
mapper.write_prg(0x6000, 0x00); // attempt to reset Q → must be rejected
assert_eq!(
mapper.read_prg(0xE000),
15,
"Q write must be rejected when PRG-RAM is write-protected (outer_reg stays 3)"
);
// Fully disable PRG-RAM ($A001=0x00)
mapper.write_prg(0xA001, 0x00);
mapper.write_prg(0x6000, 0x00); // attempt → must be rejected
assert_eq!(
mapper.read_prg(0xE000),
15,
"Q write must be rejected when PRG-RAM is disabled"
);
}
// --- No actual PRG-RAM at $6000 ---
/// Reads from $6000-$7FFF must return 0 (no PRG-RAM window; only outer register).
#[test]
fn no_actual_prg_ram_at_6000() {
let mut mapper = make_mapper();
// Write a recognisable value; the outer register stores it but the address space is silent
mapper.write_prg(0x6000, 0xFF);
assert_eq!(
mapper.read_prg(0x6000),
0,
"$6000 read must return 0 (no PRG-RAM)"
);
assert_eq!(
mapper.read_prg(0x7FFF),
0,
"$7FFF read must return 0 (no PRG-RAM)"
);
}
// --- IRQ delegation ---
#[test]
fn mmc3_irq_delegated() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 1); // IRQ latch = 1
mapper.write_prg(0xC001, 0); // reload
mapper.write_prg(0xE001, 0); // enable IRQ
// Two A12 rising edges with 3 CPU cycles low between each
for _ in 0..2 {
mapper.ppu_address_changed(0x0FFF);
for _ in 0..3 {
mapper.cpu_cycle();
}
mapper.ppu_address_changed(0x1000);
}
assert!(mapper.irq_pending(), "MMC3 IRQ must fire via mapper 37");
}
// --- Snapshot round-trip ---
#[test]
fn registers_snapshot_round_trips() {
let mut mapper = make_mapper();
// Set Q=3 (requires PRG-RAM writable at power-on)
mapper.write_prg(0x6000, 0x03);
let snap = mapper.registers_snapshot();
// Restore into a fresh mapper and verify Q=3 behaviour
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(1024, CHR_1K_BANKS);
let mut mapper2 = create_mapper(MapperContext::new_for_test(
37,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 37 must be creatable");
mapper2.restore_registers(&snap);
assert_eq!(
mapper2.read_prg(0xE000),
15,
"After restore with Q=3: fixed-last PRG must be 15"
);
}
}