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
//! Mapper 134 — MMC3-clone multicart (T4A54A / WX-KB4K / BS-5652)
//!
//! Specifications:
//! - NESdev wiki: <https://www.nesdev.org/wiki/INES_Mapper_134>
//!
//! This mapper wraps the standard MMC3 with an extra outer-bank register at
//! `$6001` (address mask `$E003`) that extends the addressable PRG and CHR
//! ROM beyond the 256 KiB / 256 KiB limit of the base MMC3:
//!
//! | Bit | Function |
//! |------|------------------------------------------------|
//! | `1` | PRG outer bank bit — shifts PRG bank by 32 (×256 KiB) |
//! | `5` | CHR outer bank bit — shifts CHR 1 KiB bank by 256 (×256 KiB) |
//!
//! All standard MMC3 registers (`$8000-$FFFF`, mask `$E001`) are fully
//! functional. The MMC3-compatible scanline IRQ is inherited unchanged.
//!
//! The NESdev wiki also documents a Mode register (`$6000`) with CNROM, NROM,
//! register-lock, and solder-pad bits, and additional outer bits for A18/A19.
//! Those modes are not implemented here; only the outer-bank bits of `$6001`
//! needed by the known ROM files are supported.
//!
//! Known limitations:
//! - CNROM CHR banking mode (`$6000` bit 2) is not implemented.
//! - NROM PRG mode (`$6001` bit 7) is not implemented.
//! - Register lock (`$6000` bit 7) is not implemented.
//! - PRG A19 / CHR A19 (`$6000` bits 4/5) are not implemented.
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
use crate::nes::cartridge::nintendo::mmc3::MMC3Mapper;
const MAPPER_NUMBER: u16 = 134;
/// Mapper 134 — MMC3 multicart with one outer-bank register at `$6001`.
pub struct Mapper134 {
mmc3: MMC3Mapper,
/// Outer-bank register (written at `$6001`, mask `$E003`).
outer: u8,
}
impl Mapper134 {
const PRG_BANK_MASK: usize = 0x2000 - 1; // 8 KiB
const CHR_1K_BANK_MASK: usize = 0x0400 - 1; // 1 KiB
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
Self {
mmc3: MMC3Mapper::new_with_irq_mode_and_prg_ram_banks(
ctx.prg_rom,
ctx.chr_rom,
ctx.mirroring,
false,
0,
),
outer: 0,
}
}
/// Adjust an MMC3 inner 8 KiB PRG bank index with the outer bank bit.
///
/// `outer` bit 1 shifts the bank by 32 (i.e. selects the second 256 KiB
/// PRG block), matching Mesen's `MMC3_134` implementation.
fn prg_bank_adjusted(&self, inner: usize) -> usize {
let outer_prg = ((self.outer & 0x02) as usize) << 4; // bit 1 → bit 5
(inner & 0x1F) | outer_prg
}
/// Adjust an MMC3 inner 1 KiB CHR bank index with the outer bank bit.
///
/// `outer` bit 5 shifts the bank by 256 (i.e. selects the second 256 KiB
/// CHR block), matching Mesen's `MMC3_134` implementation.
fn chr_bank_adjusted(&self, inner: usize) -> usize {
let outer_chr = ((self.outer & 0x20) as usize) << 3; // bit 5 → bit 8
(inner & 0xFF) | outer_chr
}
}
impl Mapper for Mapper134 {
fn base(&self) -> &BaseMapper {
&self.mmc3.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.mmc3.base
}
fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
Some(&self.mmc3)
}
fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
Some(&mut self.mmc3)
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn read_prg(&self, addr: u16) -> u8 {
if !(0x8000..=0xFFFF).contains(&addr) {
return 0;
}
let raw = self.mmc3.mapped_prg_bank(addr);
let bank = self.prg_bank_adjusted(raw);
let offset = (addr as usize) & Self::PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if (0x8000..=0xFFFF).contains(&addr) {
return self.read_prg(addr);
}
open_bus
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (addr & 0xE003) == 0x6001 {
self.outer = value;
} else {
self.mmc3.write_prg(addr, value);
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
let raw = self.mmc3.mapped_chr_1k_bank(addr);
let bank = self.chr_bank_adjusted(raw);
let offset = (addr as usize) & Self::CHR_1K_BANK_MASK;
self.mmc3.read_chr_1k_at(bank, offset)
}
fn write_chr(&mut self, addr: u16, value: u8) {
let raw = self.mmc3.mapped_chr_1k_bank(addr);
let bank = self.chr_bank_adjusted(raw);
let offset = (addr as usize) & Self::CHR_1K_BANK_MASK;
self.mmc3.write_chr_1k_at(bank, offset, value);
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
fn wram_size(&self) -> usize {
0
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.mmc3.registers_snapshot();
snap.push(self.outer);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 17 {
if let Some((&outer, mmc3_data)) = data.split_last() {
self.outer = outer;
self.mmc3.restore_registers(mmc3_data);
}
} else {
self.outer = 0;
self.mmc3.restore_registers(data);
}
}
fn ppu_address_changed(&mut self, addr: u16) {
self.mmc3.ppu_address_changed(addr);
}
fn irq_pending(&self) -> bool {
self.mmc3.irq_pending()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
// 512 KiB PRG (64 × 8 KiB) and 512 KiB CHR (512 × 1 KiB) to exercise
// both the inner (256 KiB) and outer (×2) bank ranges.
const PRG_BANKS_8K: usize = 64;
fn make_mapper() -> Mapper134 {
Mapper134::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(8 * 1024, PRG_BANKS_8K),
chr_two_blocks(),
NametableLayout::Vertical,
))
}
/// 512 × 1KiB CHR-ROM split into two distinct 256KiB blocks.
/// First block (banks 0-255): each bank starts with 0x00.
/// Second block (banks 256-511): each bank starts with 0x42.
/// This lets tests distinguish accesses to the two outer-bank regions
/// even though bank 256 % 256 == 0 (which would cause a false-pass with
/// `banked_data`).
fn chr_two_blocks() -> Vec<u8> {
let bank_size = 1024usize;
let mut chr = vec![0u8; 512 * bank_size];
for bank in 256..512 {
chr[bank * bank_size] = 0x42;
}
chr
}
// ── Factory registration ──────────────────────────────────────────────────
#[test]
fn mapper_134_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(8 * 1024, PRG_BANKS_8K),
chr_two_blocks(),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 134 must be creatable via factory");
}
// ── MMC3 inner PRG banking (without outer bit) ────────────────────────────
#[test]
fn mmc3_prg_banking_works_in_first_block() {
let mut mapper = make_mapper();
// MMC3 bank register 6 → $8000-$9FFF, bank register 7 → $A000-$BFFF
// Write 5 to bank slot 6 ($8000 window)
mapper.write_prg(0x8000, 0x86); // command: select register 6
mapper.write_prg(0x8001, 5); // bank value = 5
assert_eq!(mapper.read_prg(0x8000), 5, "inner PRG bank 5 at $8000");
}
// ── Outer PRG bank bit ────────────────────────────────────────────────────
#[test]
fn outer_reg_bit1_shifts_prg_to_second_256k_block() {
let mut mapper = make_mapper();
// Select inner PRG bank 0 for $8000 window
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 0);
assert_eq!(mapper.read_prg(0x8000), 0, "inner bank 0 → PRG bank 0");
// Set outer bit 1: bank becomes 0 | (1 << 5) = 32
mapper.write_prg(0x6001, 0x02);
assert_eq!(
mapper.read_prg(0x8000),
32,
"outer bit 1 shifts PRG bank by 32"
);
}
#[test]
fn outer_prg_bit_combines_with_inner_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 3); // inner bank 3
mapper.write_prg(0x6001, 0x02); // outer bit 1 set → shift 32
assert_eq!(mapper.read_prg(0x8000), 35, "inner 3 + outer 32 = bank 35");
}
// ── Outer CHR bank bit ────────────────────────────────────────────────────
#[test]
fn outer_reg_bit5_shifts_chr_to_second_256k_block() {
let mut mapper = make_mapper();
// MMC3 CHR register 0 → 2 KiB at PPU $0000; default inner bank = 0
mapper.write_prg(0x8000, 0x80); // command: select CHR register 0
mapper.write_prg(0x8001, 0); // inner CHR bank 0 (maps 2 × 1 KiB)
assert_eq!(
mapper.read_chr(0x0000),
0x00,
"inner CHR bank 0 → first block byte"
);
// Set outer bit 5: adjusted bank = 0 | 256 = 256 (second block → 0x42)
mapper.write_prg(0x6001, 0x20);
assert_eq!(
mapper.read_chr(0x0000),
0x42,
"outer bit 5 shifts CHR to second block"
);
}
// ── Outer register address mask $E003 ─────────────────────────────────────
#[test]
fn write_to_6001_activates_outer_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 0);
mapper.write_prg(0x6001, 0x02); // outer PRG bit set
assert_eq!(mapper.read_prg(0x8000), 32, "outer register at $6001");
}
#[test]
fn write_to_6000_does_not_change_outer_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 0);
mapper.write_prg(0x6001, 0x02); // outer PRG bit
mapper.write_prg(0x6000, 0xFF); // mode register — ignored in this impl
// outer register should still have 0x02
assert_eq!(
mapper.read_prg(0x8000),
32,
"$6000 write must not touch outer register"
);
}
// ── Snapshot / restore ────────────────────────────────────────────────────
#[test]
fn snapshot_restore_preserves_outer_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 0);
mapper.write_prg(0x6001, 0x02);
let snap = mapper.registers_snapshot();
assert_eq!(
snap.len(),
17,
"snapshot must be 17 bytes (16 MMC3 + 1 outer)"
);
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x8000),
32,
"outer register must be restored"
);
}
#[test]
fn legacy_16_byte_snapshot_resets_outer_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x86);
mapper.write_prg(0x8001, 3);
mapper.write_prg(0x6001, 0x02); // outer bit set → bank 35
// Build a legacy-size (16-byte) MMC3 snapshot
let legacy_snap: Vec<u8> = mapper.registers_snapshot()[..16].to_vec();
// Restore from a mapper that had outer=0x02 active
let mut target = make_mapper();
target.write_prg(0x8000, 0x86);
target.write_prg(0x8001, 0);
target.write_prg(0x6001, 0x02); // outer bit set
target.restore_registers(&legacy_snap);
// outer must be reset to 0 — bank 3 from MMC3 inner, no outer shift
assert_eq!(
target.read_prg(0x8000),
3,
"legacy restore must reset outer to 0"
);
}
// ── IRQ inherited from MMC3 ───────────────────────────────────────────────
#[test]
fn irq_not_pending_at_startup() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "no IRQ at power-on");
}
}