diff --git a/src/cpu.js b/src/cpu.js index 81703856..ef325641 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -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; @@ -182,12 +183,14 @@ 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; } } @@ -195,7 +198,12 @@ class CPU { 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; diff --git a/src/mappers/mapper0.js b/src/mappers/mapper0.js index cd372e4b..18c75aad 100755 --- a/src/mappers/mapper0.js +++ b/src/mappers/mapper0.js @@ -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, diff --git a/src/mappers/mapper5.js b/src/mappers/mapper5.js index 6a07914e..0910278a 100644 --- a/src/mappers/mapper5.js +++ b/src/mappers/mapper5.js @@ -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) { diff --git a/src/ppu/index.js b/src/ppu/index.js index a69b917d..5af2c223 100644 --- a/src/ppu/index.js +++ b/src/ppu/index.js @@ -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)}`); } @@ -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; diff --git a/test/mappers.spec.js b/test/mappers.spec.js index e18a2e78..0563b925 100644 --- a/test/mappers.spec.js +++ b/test/mappers.spec.js @@ -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) { @@ -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);