Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/cpu.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ class CPU {

// Check IRQ/reset at the start of each instruction.
if (this.irqRequested) {
let clearIrqRequest = false;
temp = this.getStatus();

this.REG_PC_NEW = this.REG_PC;
Expand All @@ -182,20 +183,27 @@ class CPU {
// Clear the B flag (bit 4) for hardware interrupts
this.doIrq(temp & 0xef);
interruptCycles = 7;
clearIrqRequest = true;
break;
}
case 2: {
// Reset:
this.doResetInterrupt();
interruptCycles = 7;
clearIrqRequest = true;
break;
}
}

this.REG_PC = this.REG_PC_NEW;
this.F_INTERRUPT = this.F_INTERRUPT_NEW;
this.F_BRK = this.F_BRK_NEW;
this.irqRequested = false;
// Leave masked IRQs latched. MMC5 can retrigger an IRQ while the
// handler still has I=1, and clearing the request here drops the
// follow-up interrupt that should fire after RTI restores I=0.
if (clearIrqRequest) {
this.irqRequested = false;
}
}

if (this.nes.mmap === null) return 32;
Expand Down
9 changes: 9 additions & 0 deletions src/mappers/mapper0.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,15 @@ class Mapper0 {
return null;
}

// Called by the PPU for mirrored nametable writes after the address has
// been translated to its backing source. Return true if the mapper handled
// the write and the PPU should skip its default nametable cache update.
// MMC5 overrides this for ExRAM/fill-mode nametable sources.
// eslint-disable-next-line no-unused-vars
writePpuMemory(address, value) {
return false;
}

// Look up a sprite pattern tile by ptTile index (0-511).
// Default: return from the PPU's current ptTile cache.
// MMC5 overrides this to look up from Set A's VROM banks directly,
Expand Down
61 changes: 61 additions & 0 deletions src/mappers/mapper5.js
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,67 @@ class Mapper5 extends Mapper0 {
return { tile, attrib };
}

_getFillAttrByte() {
return (
this.fillAttr |
(this.fillAttr << 2) |
(this.fillAttr << 4) |
(this.fillAttr << 6)
);
}

_writeNametableSource(source, offset, value) {
let ppu = this.nes.ppu;
let address = 0x2000 + (source << 10) + offset;
let isAttrib = offset >= 0x3c0;
let data = value;

switch (source) {
case 0:
case 1:
break;

case 2:
// ExRAM-backed nametable. In modes 0/1 the PPU can read/write it via
// $2006/$2007 during blanking, so keep ExRAM and the cached NameTable
// in sync. In modes 2/3 nametable reads see zeros, so writes do not
// persist to ExRAM and the visible data remains zero.
if (this.exramMode < 2) {
this.exram[offset] = value;
} else {
data = 0x00;
}
break;

case 3:
// Fill mode is generated from $5106/$5107, not writable VRAM. Ignore
// the attempted write and preserve the synthetic fill byte instead.
data = isAttrib ? this._getFillAttrByte() : this.fillTile;
break;

default:
return;
}

ppu.vramMem[address] = data;
if (isAttrib) {
ppu.attribTableWrite(source, offset - 0x3c0, data);
} else {
ppu.nameTableWrite(source, offset, data);
}
}

writePpuMemory(address, value) {
if (address < 0x2000 || address >= 0x3000) {
return false;
}

let source = (address - 0x2000) >> 10;
let offset = address & 0x03ff;
this._writeNametableSource(source, offset, value);
return true;
}

// --- ROM Loading ---
loadROM() {
if (!this.nes.rom.valid) {
Expand Down
9 changes: 7 additions & 2 deletions src/ppu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,12 @@ class PPU {
} else {
// Use lookup table for mirrored address:
if (address < this.vramMirrorTable.length) {
this.writeMem(this.vramMirrorTable[address], value);
let mappedAddress = this.vramMirrorTable[address];
// Let the mapper handle custom nametable backends such as MMC5 ExRAM
// and fill mode. Otherwise fall back to the standard PPU write path.
if (!this.nes.mmap.writePpuMemory(mappedAddress, value)) {
this.writeMem(mappedAddress, value);
}
} else {
throw new Error(`Invalid VRAM address: ${address.toString(16)}`);
}
Expand Down Expand Up @@ -1730,7 +1735,7 @@ class PPU {
// top tile is (index & $FE), bottom tile is (index & $FE) + 1.
let sprBaseAddr = (sprTile & 1) !== 0 ? 0x1000 : 0x0000;
let topTileNum = sprTile & 0xfe;
let top = (sprTile & 1) !== 0 ? topTileNum - 1 + 256 : topTileNum;
let top = topTileNum + ((sprTile & 1) !== 0 ? 256 : 0);

let dy = sprY + 1;
let fineY = scan - dy;
Expand Down
41 changes: 41 additions & 0 deletions test/mappers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ function createMockNes() {
this.vramMirrorTable[fromStart + i] = toStart + i;
}
},
nameTableWrite: function (index, address, value) {
this.nameTable[index].tile[address] = value;
},
attribTableWrite: function (index, address, value) {
this.nameTable[index].writeAttrib(address, value);
this.nameTable[index].tile[0x3c0 + address] = value;
},
},
papu: {
getLengthMax: function (value) {
Expand Down Expand Up @@ -201,6 +208,40 @@ describe("MMC5 (Mapper 5)", function () {
});
});

describe("PPU nametable writes", function () {
it("writes ExRAM-backed nametable bytes to ExRAM and NameTable 2 only", function () {
// slot0=ExRAM, slot1=CIRAM B, slot2=CIRAM A, slot3=CIRAM B
mapper.write(0x5105, 0x46);

let mapped = mockNes.ppu.vramMirrorTable[0x2000];
assert.strictEqual(mapped, 0x2800);
assert.strictEqual(mockNes.ppu.ntable1[2], 0);

mapper.writePpuMemory(mapped, 0x5a);

assert.strictEqual(mapper.exram[0], 0x5a);
assert.strictEqual(mockNes.ppu.vramMem[0x2800], 0x5a);
assert.strictEqual(mockNes.ppu.nameTable[2].tile[0], 0x5a);
assert.strictEqual(mockNes.ppu.nameTable[0].tile[0], 0x00);
assert.strictEqual(mockNes.ppu.nameTable[1].tile[0], 0x00);
});

it("ignores writes to fill-mode nametable sources", function () {
mapper.write(0x5106, 0x33);
mapper.write(0x5107, 0x02);
// slot0=fill, slot1=CIRAM B, slot2=CIRAM A, slot3=CIRAM B
mapper.write(0x5105, 0x47);

let mapped = mockNes.ppu.vramMirrorTable[0x2000];
assert.strictEqual(mapped, 0x2c00);

mapper.writePpuMemory(mapped, 0x99);

assert.strictEqual(mockNes.ppu.vramMem[0x2c00], 0x33);
assert.strictEqual(mockNes.ppu.nameTable[3].tile[0], 0x33);
});
});

describe("scanline IRQ ($5203/$5204)", function () {
it("reports no IRQ pending and no in-frame initially", function () {
let val = mapper.load(0x5204);
Expand Down
Loading