var Tile = require("./tile"); var utils = require("./utils"); var PPU = function(nes) { this.nes = nes; // Keep Chrome happy this.vramMem = null; this.spriteMem = null; this.vramAddress = null; this.vramTmpAddress = null; this.vramBufferedReadValue = null; this.firstWrite = null; this.sramAddress = null; this.currentMirroring = null; this.requestEndFrame = null; this.nmiOk = null; this.dummyCycleToggle = null; this.validTileData = null; this.nmiCounter = null; this.scanlineAlreadyRendered = null; this.f_nmiOnVblank = null; this.f_spriteSize = null; this.f_bgPatternTable = null; this.f_spPatternTable = null; this.f_addrInc = null; this.f_nTblAddress = null; this.f_color = null; this.f_spVisibility = null; this.f_bgVisibility = null; this.f_spClipping = null; this.f_bgClipping = null; this.f_dispType = null; this.cntFV = null; this.cntV = null; this.cntH = null; this.cntVT = null; this.cntHT = null; this.regFV = null; this.regV = null; this.regH = null; this.regVT = null; this.regHT = null; this.regFH = null; this.regS = null; this.curNt = null; this.attrib = null; this.buffer = null; this.bgbuffer = null; this.pixrendered = null; this.validTileData = null; this.scantile = null; this.scanline = null; this.lastRenderedScanline = null; this.curX = null; this.sprX = null; this.sprY = null; this.sprTile = null; this.sprCol = null; this.vertFlip = null; this.horiFlip = null; this.bgPriority = null; this.spr0HitX = null; this.spr0HitY = null; this.hitSpr0 = null; this.sprPalette = null; this.imgPalette = null; this.ptTile = null; this.ntable1 = null; this.currentMirroring = null; this.nameTable = null; this.vramMirrorTable = null; this.palTable = null; // Rendering Options: this.showSpr0Hit = false; this.clipToTvSize = true; this.reset(); }; PPU.prototype = { // Status flags: STATUS_VRAMWRITE: 4, STATUS_SLSPRITECOUNT: 5, STATUS_SPRITE0HIT: 6, STATUS_VBLANK: 7, reset: function() { var i; // Memory this.vramMem = new Array(0x8000); this.spriteMem = new Array(0x100); for (i = 0; i < this.vramMem.length; i++) { this.vramMem[i] = 0; } for (i = 0; i < this.spriteMem.length; i++) { this.spriteMem[i] = 0; } // VRAM I/O: this.vramAddress = null; this.vramTmpAddress = null; this.vramBufferedReadValue = 0; this.firstWrite = true; // VRAM/Scroll Hi/Lo latch // SPR-RAM I/O: this.sramAddress = 0; // 8-bit only. this.currentMirroring = -1; this.requestEndFrame = false; this.nmiOk = false; this.dummyCycleToggle = false; this.validTileData = false; this.nmiCounter = 0; this.scanlineAlreadyRendered = null; // Control Flags Register 1: this.f_nmiOnVblank = 0; // NMI on VBlank. 0=disable, 1=enable this.f_spriteSize = 0; // Sprite size. 0=8x8, 1=8x16 this.f_bgPatternTable = 0; // Background Pattern Table address. 0=0x0000,1=0x1000 this.f_spPatternTable = 0; // Sprite Pattern Table address. 0=0x0000,1=0x1000 this.f_addrInc = 0; // PPU Address Increment. 0=1,1=32 this.f_nTblAddress = 0; // Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00 // Control Flags Register 2: this.f_color = 0; // Background color. 0=black, 1=blue, 2=green, 4=red this.f_spVisibility = 0; // Sprite visibility. 0=not displayed,1=displayed this.f_bgVisibility = 0; // Background visibility. 0=Not Displayed,1=displayed this.f_spClipping = 0; // Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping this.f_bgClipping = 0; // Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping this.f_dispType = 0; // Display type. 0=color, 1=monochrome // Counters: this.cntFV = 0; this.cntV = 0; this.cntH = 0; this.cntVT = 0; this.cntHT = 0; // Registers: this.regFV = 0; this.regV = 0; this.regH = 0; this.regVT = 0; this.regHT = 0; this.regFH = 0; this.regS = 0; // These are temporary variables used in rendering and sound procedures. // Their states outside of those procedures can be ignored. // TODO: the use of this is a bit weird, investigate this.curNt = null; // Variables used when rendering: this.attrib = new Array(32); this.buffer = new Array(256 * 240); this.bgbuffer = new Array(256 * 240); this.pixrendered = new Array(256 * 240); this.validTileData = null; this.scantile = new Array(32); // Initialize misc vars: this.scanline = 0; this.lastRenderedScanline = -1; this.curX = 0; // Sprite data: this.sprX = new Array(64); // X coordinate this.sprY = new Array(64); // Y coordinate this.sprTile = new Array(64); // Tile Index (into pattern table) this.sprCol = new Array(64); // Upper two bits of color this.vertFlip = new Array(64); // Vertical Flip this.horiFlip = new Array(64); // Horizontal Flip this.bgPriority = new Array(64); // Background priority this.spr0HitX = 0; // Sprite #0 hit X coordinate this.spr0HitY = 0; // Sprite #0 hit Y coordinate this.hitSpr0 = false; // Palette data: this.sprPalette = new Array(16); this.imgPalette = new Array(16); // Create pattern table tile buffers: this.ptTile = new Array(512); for (i = 0; i < 512; i++) { this.ptTile[i] = new Tile(); } // Create nametable buffers: // Name table data: this.ntable1 = new Array(4); this.currentMirroring = -1; this.nameTable = new Array(4); for (i = 0; i < 4; i++) { this.nameTable[i] = new NameTable(32, 32, "Nt" + i); } // Initialize mirroring lookup table: this.vramMirrorTable = new Array(0x8000); for (i = 0; i < 0x8000; i++) { this.vramMirrorTable[i] = i; } this.palTable = new PaletteTable(); this.palTable.loadNTSCPalette(); //this.palTable.loadDefaultPalette(); this.updateControlReg1(0); this.updateControlReg2(0); }, // Sets Nametable mirroring. setMirroring: function(mirroring) { if (mirroring === this.currentMirroring) { return; } this.currentMirroring = mirroring; this.triggerRendering(); // Remove mirroring: if (this.vramMirrorTable === null) { this.vramMirrorTable = new Array(0x8000); } for (var i = 0; i < 0x8000; i++) { this.vramMirrorTable[i] = i; } // Palette mirroring: this.defineMirrorRegion(0x3f20, 0x3f00, 0x20); this.defineMirrorRegion(0x3f40, 0x3f00, 0x20); this.defineMirrorRegion(0x3f80, 0x3f00, 0x20); this.defineMirrorRegion(0x3fc0, 0x3f00, 0x20); // Additional mirroring: this.defineMirrorRegion(0x3000, 0x2000, 0xf00); this.defineMirrorRegion(0x4000, 0x0000, 0x4000); if (mirroring === this.nes.rom.HORIZONTAL_MIRRORING) { // Horizontal mirroring. this.ntable1[0] = 0; this.ntable1[1] = 0; this.ntable1[2] = 1; this.ntable1[3] = 1; this.defineMirrorRegion(0x2400, 0x2000, 0x400); this.defineMirrorRegion(0x2c00, 0x2800, 0x400); } else if (mirroring === this.nes.rom.VERTICAL_MIRRORING) { // Vertical mirroring. this.ntable1[0] = 0; this.ntable1[1] = 1; this.ntable1[2] = 0; this.ntable1[3] = 1; this.defineMirrorRegion(0x2800, 0x2000, 0x400); this.defineMirrorRegion(0x2c00, 0x2400, 0x400); } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING) { // Single Screen mirroring this.ntable1[0] = 0; this.ntable1[1] = 0; this.ntable1[2] = 0; this.ntable1[3] = 0; this.defineMirrorRegion(0x2400, 0x2000, 0x400); this.defineMirrorRegion(0x2800, 0x2000, 0x400); this.defineMirrorRegion(0x2c00, 0x2000, 0x400); } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING2) { this.ntable1[0] = 1; this.ntable1[1] = 1; this.ntable1[2] = 1; this.ntable1[3] = 1; this.defineMirrorRegion(0x2400, 0x2400, 0x400); this.defineMirrorRegion(0x2800, 0x2400, 0x400); this.defineMirrorRegion(0x2c00, 0x2400, 0x400); } else { // Assume Four-screen mirroring. this.ntable1[0] = 0; this.ntable1[1] = 1; this.ntable1[2] = 2; this.ntable1[3] = 3; } }, // Define a mirrored area in the address lookup table. // Assumes the regions don't overlap. // The 'to' region is the region that is physically in memory. defineMirrorRegion: function(fromStart, toStart, size) { for (var i = 0; i < size; i++) { this.vramMirrorTable[fromStart + i] = toStart + i; } }, startVBlank: function() { // Do NMI: this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); // Make sure everything is rendered: if (this.lastRenderedScanline < 239) { this.renderFramePartially( this.lastRenderedScanline + 1, 240 - this.lastRenderedScanline ); } // End frame: this.endFrame(); // Reset scanline counter: this.lastRenderedScanline = -1; }, endScanline: function() { switch (this.scanline) { case 19: // Dummy scanline. // May be variable length: if (this.dummyCycleToggle) { // Remove dead cycle at end of scanline, // for next scanline: this.curX = 1; this.dummyCycleToggle = !this.dummyCycleToggle; } break; case 20: // Clear VBlank flag: this.setStatusFlag(this.STATUS_VBLANK, false); // Clear Sprite #0 hit flag: this.setStatusFlag(this.STATUS_SPRITE0HIT, false); this.hitSpr0 = false; this.spr0HitX = -1; this.spr0HitY = -1; if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { // Update counters: this.cntFV = this.regFV; this.cntV = this.regV; this.cntH = this.regH; this.cntVT = this.regVT; this.cntHT = this.regHT; if (this.f_bgVisibility === 1) { // Render dummy scanline: this.renderBgScanline(false, 0); } } if (this.f_bgVisibility === 1 && this.f_spVisibility === 1) { // Check sprite 0 hit for first scanline: this.checkSprite0(0); } if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { // Clock mapper IRQ Counter: this.nes.mmap.clockIrqCounter(); } break; case 261: // Dead scanline, no rendering. // Set VINT: this.setStatusFlag(this.STATUS_VBLANK, true); this.requestEndFrame = true; this.nmiCounter = 9; // Wrap around: this.scanline = -1; // will be incremented to 0 break; default: if (this.scanline >= 21 && this.scanline <= 260) { // Render normally: if (this.f_bgVisibility === 1) { if (!this.scanlineAlreadyRendered) { // update scroll: this.cntHT = this.regHT; this.cntH = this.regH; this.renderBgScanline(true, this.scanline + 1 - 21); } this.scanlineAlreadyRendered = false; // Check for sprite 0 (next scanline): if (!this.hitSpr0 && this.f_spVisibility === 1) { if ( this.sprX[0] >= -7 && this.sprX[0] < 256 && this.sprY[0] + 1 <= this.scanline - 20 && this.sprY[0] + 1 + (this.f_spriteSize === 0 ? 8 : 16) >= this.scanline - 20 ) { if (this.checkSprite0(this.scanline - 20)) { this.hitSpr0 = true; } } } } if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { // Clock mapper IRQ Counter: this.nes.mmap.clockIrqCounter(); } } } this.scanline++; this.regsToAddress(); this.cntsToAddress(); }, startFrame: function() { // Set background color: var bgColor = 0; if (this.f_dispType === 0) { // Color display. // f_color determines color emphasis. // Use first entry of image palette as BG color. bgColor = this.imgPalette[0]; } else { // Monochrome display. // f_color determines the bg color. switch (this.f_color) { case 0: // Black bgColor = 0x00000; break; case 1: // Green bgColor = 0x00ff00; break; case 2: // Blue bgColor = 0xff0000; break; case 3: // Invalid. Use black. bgColor = 0x000000; break; case 4: // Red bgColor = 0x0000ff; break; default: // Invalid. Use black. bgColor = 0x0; } } var buffer = this.buffer; var i; for (i = 0; i < 256 * 240; i++) { buffer[i] = bgColor; } var pixrendered = this.pixrendered; for (i = 0; i < pixrendered.length; i++) { pixrendered[i] = 65; } }, endFrame: function() { var i, x, y; var buffer = this.buffer; // Draw spr#0 hit coordinates: if (this.showSpr0Hit) { // Spr 0 position: if ( this.sprX[0] >= 0 && this.sprX[0] < 256 && this.sprY[0] >= 0 && this.sprY[0] < 240 ) { for (i = 0; i < 256; i++) { buffer[(this.sprY[0] << 8) + i] = 0xff5555; } for (i = 0; i < 240; i++) { buffer[(i << 8) + this.sprX[0]] = 0xff5555; } } // Hit position: if ( this.spr0HitX >= 0 && this.spr0HitX < 256 && this.spr0HitY >= 0 && this.spr0HitY < 240 ) { for (i = 0; i < 256; i++) { buffer[(this.spr0HitY << 8) + i] = 0x55ff55; } for (i = 0; i < 240; i++) { buffer[(i << 8) + this.spr0HitX] = 0x55ff55; } } } // This is a bit lazy.. // if either the sprites or the background should be clipped, // both are clipped after rendering is finished. if ( this.clipToTvSize || this.f_bgClipping === 0 || this.f_spClipping === 0 ) { // Clip left 8-pixels column: for (y = 0; y < 240; y++) { for (x = 0; x < 8; x++) { buffer[(y << 8) + x] = 0; } } } if (this.clipToTvSize) { // Clip right 8-pixels column too: for (y = 0; y < 240; y++) { for (x = 0; x < 8; x++) { buffer[(y << 8) + 255 - x] = 0; } } } // Clip top and bottom 8 pixels: if (this.clipToTvSize) { for (y = 0; y < 8; y++) { for (x = 0; x < 256; x++) { buffer[(y << 8) + x] = 0; buffer[((239 - y) << 8) + x] = 0; } } } this.nes.ui.writeFrame(buffer); }, updateControlReg1: function(value) { this.triggerRendering(); this.f_nmiOnVblank = (value >> 7) & 1; this.f_spriteSize = (value >> 5) & 1; this.f_bgPatternTable = (value >> 4) & 1; this.f_spPatternTable = (value >> 3) & 1; this.f_addrInc = (value >> 2) & 1; this.f_nTblAddress = value & 3; this.regV = (value >> 1) & 1; this.regH = value & 1; this.regS = (value >> 4) & 1; }, updateControlReg2: function(value) { this.triggerRendering(); this.f_color = (value >> 5) & 7; this.f_spVisibility = (value >> 4) & 1; this.f_bgVisibility = (value >> 3) & 1; this.f_spClipping = (value >> 2) & 1; this.f_bgClipping = (value >> 1) & 1; this.f_dispType = value & 1; if (this.f_dispType === 0) { this.palTable.setEmphasis(this.f_color); } this.updatePalettes(); }, setStatusFlag: function(flag, value) { var n = 1 << flag; this.nes.cpu.mem[0x2002] = (this.nes.cpu.mem[0x2002] & (255 - n)) | (value ? n : 0); }, // CPU Register $2002: // Read the Status Register. readStatusRegister: function() { var tmp = this.nes.cpu.mem[0x2002]; // Reset scroll & VRAM Address toggle: this.firstWrite = true; // Clear VBlank flag: this.setStatusFlag(this.STATUS_VBLANK, false); // Fetch status data: return tmp; }, // CPU Register $2003: // Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map) writeSRAMAddress: function(address) { this.sramAddress = address; }, // CPU Register $2004 (R): // Read from SPR-RAM (Sprite RAM). // The address should be set first. sramLoad: function() { /*short tmp = sprMem.load(sramAddress); sramAddress++; // Increment address sramAddress%=0x100; return tmp;*/ return this.spriteMem[this.sramAddress]; }, // CPU Register $2004 (W): // Write to SPR-RAM (Sprite RAM). // The address should be set first. sramWrite: function(value) { this.spriteMem[this.sramAddress] = value; this.spriteRamWriteUpdate(this.sramAddress, value); this.sramAddress++; // Increment address this.sramAddress %= 0x100; }, // CPU Register $2005: // Write to scroll registers. // The first write is the vertical offset, the second is the // horizontal offset: scrollWrite: function(value) { this.triggerRendering(); if (this.firstWrite) { // First write, horizontal scroll: this.regHT = (value >> 3) & 31; this.regFH = value & 7; } else { // Second write, vertical scroll: this.regFV = value & 7; this.regVT = (value >> 3) & 31; } this.firstWrite = !this.firstWrite; }, // CPU Register $2006: // Sets the adress used when reading/writing from/to VRAM. // The first write sets the high byte, the second the low byte. writeVRAMAddress: function(address) { if (this.firstWrite) { this.regFV = (address >> 4) & 3; this.regV = (address >> 3) & 1; this.regH = (address >> 2) & 1; this.regVT = (this.regVT & 7) | ((address & 3) << 3); } else { this.triggerRendering(); this.regVT = (this.regVT & 24) | ((address >> 5) & 7); this.regHT = address & 31; this.cntFV = this.regFV; this.cntV = this.regV; this.cntH = this.regH; this.cntVT = this.regVT; this.cntHT = this.regHT; this.checkSprite0(this.scanline - 20); } this.firstWrite = !this.firstWrite; // Invoke mapper latch: this.cntsToAddress(); if (this.vramAddress < 0x2000) { this.nes.mmap.latchAccess(this.vramAddress); } }, // CPU Register $2007(R): // Read from PPU memory. The address should be set first. vramLoad: function() { var tmp; this.cntsToAddress(); this.regsToAddress(); // If address is in range 0x0000-0x3EFF, return buffered values: if (this.vramAddress <= 0x3eff) { tmp = this.vramBufferedReadValue; // Update buffered value: if (this.vramAddress < 0x2000) { this.vramBufferedReadValue = this.vramMem[this.vramAddress]; } else { this.vramBufferedReadValue = this.mirroredLoad(this.vramAddress); } // Mapper latch access: if (this.vramAddress < 0x2000) { this.nes.mmap.latchAccess(this.vramAddress); } // Increment by either 1 or 32, depending on d2 of Control Register 1: this.vramAddress += this.f_addrInc === 1 ? 32 : 1; this.cntsFromAddress(); this.regsFromAddress(); return tmp; // Return the previous buffered value. } // No buffering in this mem range. Read normally. tmp = this.mirroredLoad(this.vramAddress); // Increment by either 1 or 32, depending on d2 of Control Register 1: this.vramAddress += this.f_addrInc === 1 ? 32 : 1; this.cntsFromAddress(); this.regsFromAddress(); return tmp; }, // CPU Register $2007(W): // Write to PPU memory. The address should be set first. vramWrite: function(value) { this.triggerRendering(); this.cntsToAddress(); this.regsToAddress(); if (this.vramAddress >= 0x2000) { // Mirroring is used. this.mirroredWrite(this.vramAddress, value); } else { // Write normally. this.writeMem(this.vramAddress, value); // Invoke mapper latch: this.nes.mmap.latchAccess(this.vramAddress); } // Increment by either 1 or 32, depending on d2 of Control Register 1: this.vramAddress += this.f_addrInc === 1 ? 32 : 1; this.regsFromAddress(); this.cntsFromAddress(); }, // CPU Register $4014: // Write 256 bytes of main memory // into Sprite RAM. sramDMA: function(value) { var baseAddress = value * 0x100; var data; for (var i = this.sramAddress; i < 256; i++) { data = this.nes.cpu.mem[baseAddress + i]; this.spriteMem[i] = data; this.spriteRamWriteUpdate(i, data); } this.nes.cpu.haltCycles(513); }, // Updates the scroll registers from a new VRAM address. regsFromAddress: function() { var address = (this.vramTmpAddress >> 8) & 0xff; this.regFV = (address >> 4) & 7; this.regV = (address >> 3) & 1; this.regH = (address >> 2) & 1; this.regVT = (this.regVT & 7) | ((address & 3) << 3); address = this.vramTmpAddress & 0xff; this.regVT = (this.regVT & 24) | ((address >> 5) & 7); this.regHT = address & 31; }, // Updates the scroll registers from a new VRAM address. cntsFromAddress: function() { var address = (this.vramAddress >> 8) & 0xff; this.cntFV = (address >> 4) & 3; this.cntV = (address >> 3) & 1; this.cntH = (address >> 2) & 1; this.cntVT = (this.cntVT & 7) | ((address & 3) << 3); address = this.vramAddress & 0xff; this.cntVT = (this.cntVT & 24) | ((address >> 5) & 7); this.cntHT = address & 31; }, regsToAddress: function() { var b1 = (this.regFV & 7) << 4; b1 |= (this.regV & 1) << 3; b1 |= (this.regH & 1) << 2; b1 |= (this.regVT >> 3) & 3; var b2 = (this.regVT & 7) << 5; b2 |= this.regHT & 31; this.vramTmpAddress = ((b1 << 8) | b2) & 0x7fff; }, cntsToAddress: function() { var b1 = (this.cntFV & 7) << 4; b1 |= (this.cntV & 1) << 3; b1 |= (this.cntH & 1) << 2; b1 |= (this.cntVT >> 3) & 3; var b2 = (this.cntVT & 7) << 5; b2 |= this.cntHT & 31; this.vramAddress = ((b1 << 8) | b2) & 0x7fff; }, incTileCounter: function(count) { for (var i = count; i !== 0; i--) { this.cntHT++; if (this.cntHT === 32) { this.cntHT = 0; this.cntVT++; if (this.cntVT >= 30) { this.cntH++; if (this.cntH === 2) { this.cntH = 0; this.cntV++; if (this.cntV === 2) { this.cntV = 0; this.cntFV++; this.cntFV &= 0x7; } } } } } }, // Reads from memory, taking into account // mirroring/mapping of address ranges. mirroredLoad: function(address) { return this.vramMem[this.vramMirrorTable[address]]; }, // Writes to memory, taking into account // mirroring/mapping of address ranges. mirroredWrite: function(address, value) { if (address >= 0x3f00 && address < 0x3f20) { // Palette write mirroring. if (address === 0x3f00 || address === 0x3f10) { this.writeMem(0x3f00, value); this.writeMem(0x3f10, value); } else if (address === 0x3f04 || address === 0x3f14) { this.writeMem(0x3f04, value); this.writeMem(0x3f14, value); } else if (address === 0x3f08 || address === 0x3f18) { this.writeMem(0x3f08, value); this.writeMem(0x3f18, value); } else if (address === 0x3f0c || address === 0x3f1c) { this.writeMem(0x3f0c, value); this.writeMem(0x3f1c, value); } else { this.writeMem(address, value); } } else { // Use lookup table for mirrored address: if (address < this.vramMirrorTable.length) { this.writeMem(this.vramMirrorTable[address], value); } else { throw new Error("Invalid VRAM address: " + address.toString(16)); } } }, triggerRendering: function() { if (this.scanline >= 21 && this.scanline <= 260) { // Render sprites, and combine: this.renderFramePartially( this.lastRenderedScanline + 1, this.scanline - 21 - this.lastRenderedScanline ); // Set last rendered scanline: this.lastRenderedScanline = this.scanline - 21; } }, renderFramePartially: function(startScan, scanCount) { if (this.f_spVisibility === 1) { this.renderSpritesPartially(startScan, scanCount, true); } if (this.f_bgVisibility === 1) { var si = startScan << 8; var ei = (startScan + scanCount) << 8; if (ei > 0xf000) { ei = 0xf000; } var buffer = this.buffer; var bgbuffer = this.bgbuffer; var pixrendered = this.pixrendered; for (var destIndex = si; destIndex < ei; destIndex++) { if (pixrendered[destIndex] > 0xff) { buffer[destIndex] = bgbuffer[destIndex]; } } } if (this.f_spVisibility === 1) { this.renderSpritesPartially(startScan, scanCount, false); } this.validTileData = false; }, renderBgScanline: function(bgbuffer, scan) { var baseTile = this.regS === 0 ? 0 : 256; var destIndex = (scan << 8) - this.regFH; this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; this.cntHT = this.regHT; this.cntH = this.regH; this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; if (scan < 240 && scan - this.cntFV >= 0) { var tscanoffset = this.cntFV << 3; var scantile = this.scantile; var attrib = this.attrib; var ptTile = this.ptTile; var nameTable = this.nameTable; var imgPalette = this.imgPalette; var pixrendered = this.pixrendered; var targetBuffer = bgbuffer ? this.bgbuffer : this.buffer; var t, tpix, att, col; for (var tile = 0; tile < 32; tile++) { if (scan >= 0) { // Fetch tile & attrib data: if (this.validTileData) { // Get data from array: t = scantile[tile]; if (typeof t === "undefined") { continue; } tpix = t.pix; att = attrib[tile]; } else { // Fetch data: t = ptTile[ baseTile + nameTable[this.curNt].getTileIndex(this.cntHT, this.cntVT) ]; if (typeof t === "undefined") { continue; } tpix = t.pix; att = nameTable[this.curNt].getAttrib(this.cntHT, this.cntVT); scantile[tile] = t; attrib[tile] = att; } // Render tile scanline: var sx = 0; var x = (tile << 3) - this.regFH; if (x > -8) { if (x < 0) { destIndex -= x; sx = -x; } if (t.opaque[this.cntFV]) { for (; sx < 8; sx++) { targetBuffer[destIndex] = imgPalette[tpix[tscanoffset + sx] + att]; pixrendered[destIndex] |= 256; destIndex++; } } else { for (; sx < 8; sx++) { col = tpix[tscanoffset + sx]; if (col !== 0) { targetBuffer[destIndex] = imgPalette[col + att]; pixrendered[destIndex] |= 256; } destIndex++; } } } } // Increase Horizontal Tile Counter: if (++this.cntHT === 32) { this.cntHT = 0; this.cntH++; this.cntH %= 2; this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; } } // Tile data for one row should now have been fetched, // so the data in the array is valid. this.validTileData = true; } // update vertical scroll: this.cntFV++; if (this.cntFV === 8) { this.cntFV = 0; this.cntVT++; if (this.cntVT === 30) { this.cntVT = 0; this.cntV++; this.cntV %= 2; this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; } else if (this.cntVT === 32) { this.cntVT = 0; } // Invalidate fetched data: this.validTileData = false; } }, renderSpritesPartially: function(startscan, scancount, bgPri) { if (this.f_spVisibility === 1) { for (var i = 0; i < 64; i++) { if ( this.bgPriority[i] === bgPri && this.sprX[i] >= 0 && this.sprX[i] < 256 && this.sprY[i] + 8 >= startscan && this.sprY[i] < startscan + scancount ) { // Show sprite. if (this.f_spriteSize === 0) { // 8x8 sprites this.srcy1 = 0; this.srcy2 = 8; if (this.sprY[i] < startscan) { this.srcy1 = startscan - this.sprY[i] - 1; } if (this.sprY[i] + 8 > startscan + scancount) { this.srcy2 = startscan + scancount - this.sprY[i] + 1; } if (this.f_spPatternTable === 0) { this.ptTile[this.sprTile[i]].render( this.buffer, 0, this.srcy1, 8, this.srcy2, this.sprX[i], this.sprY[i] + 1, this.sprCol[i], this.sprPalette, this.horiFlip[i], this.vertFlip[i], i, this.pixrendered ); } else { this.ptTile[this.sprTile[i] + 256].render( this.buffer, 0, this.srcy1, 8, this.srcy2, this.sprX[i], this.sprY[i] + 1, this.sprCol[i], this.sprPalette, this.horiFlip[i], this.vertFlip[i], i, this.pixrendered ); } } else { // 8x16 sprites var top = this.sprTile[i]; if ((top & 1) !== 0) { top = this.sprTile[i] - 1 + 256; } var srcy1 = 0; var srcy2 = 8; if (this.sprY[i] < startscan) { srcy1 = startscan - this.sprY[i] - 1; } if (this.sprY[i] + 8 > startscan + scancount) { srcy2 = startscan + scancount - this.sprY[i]; } this.ptTile[top + (this.vertFlip[i] ? 1 : 0)].render( this.buffer, 0, srcy1, 8, srcy2, this.sprX[i], this.sprY[i] + 1, this.sprCol[i], this.sprPalette, this.horiFlip[i], this.vertFlip[i], i, this.pixrendered ); srcy1 = 0; srcy2 = 8; if (this.sprY[i] + 8 < startscan) { srcy1 = startscan - (this.sprY[i] + 8 + 1); } if (this.sprY[i] + 16 > startscan + scancount) { srcy2 = startscan + scancount - (this.sprY[i] + 8); } this.ptTile[top + (this.vertFlip[i] ? 0 : 1)].render( this.buffer, 0, srcy1, 8, srcy2, this.sprX[i], this.sprY[i] + 1 + 8, this.sprCol[i], this.sprPalette, this.horiFlip[i], this.vertFlip[i], i, this.pixrendered ); } } } } }, checkSprite0: function(scan) { this.spr0HitX = -1; this.spr0HitY = -1; var toffset; var tIndexAdd = this.f_spPatternTable === 0 ? 0 : 256; var x, y, t, i; var bufferIndex; x = this.sprX[0]; y = this.sprY[0] + 1; if (this.f_spriteSize === 0) { // 8x8 sprites. // Check range: if (y <= scan && y + 8 > scan && x >= -7 && x < 256) { // Sprite is in range. // Draw scanline: t = this.ptTile[this.sprTile[0] + tIndexAdd]; if (this.vertFlip[0]) { toffset = 7 - (scan - y); } else { toffset = scan - y; } toffset *= 8; bufferIndex = scan * 256 + x; if (this.horiFlip[0]) { for (i = 7; i >= 0; i--) { if (x >= 0 && x < 256) { if ( bufferIndex >= 0 && bufferIndex < 61440 && this.pixrendered[bufferIndex] !== 0 ) { if (t.pix[toffset + i] !== 0) { this.spr0HitX = bufferIndex % 256; this.spr0HitY = scan; return true; } } } x++; bufferIndex++; } } else { for (i = 0; i < 8; i++) { if (x >= 0 && x < 256) { if ( bufferIndex >= 0 && bufferIndex < 61440 && this.pixrendered[bufferIndex] !== 0 ) { if (t.pix[toffset + i] !== 0) { this.spr0HitX = bufferIndex % 256; this.spr0HitY = scan; return true; } } } x++; bufferIndex++; } } } } else { // 8x16 sprites: // Check range: if (y <= scan && y + 16 > scan && x >= -7 && x < 256) { // Sprite is in range. // Draw scanline: if (this.vertFlip[0]) { toffset = 15 - (scan - y); } else { toffset = scan - y; } if (toffset < 8) { // first half of sprite. t = this.ptTile[ this.sprTile[0] + (this.vertFlip[0] ? 1 : 0) + ((this.sprTile[0] & 1) !== 0 ? 255 : 0) ]; } else { // second half of sprite. t = this.ptTile[ this.sprTile[0] + (this.vertFlip[0] ? 0 : 1) + ((this.sprTile[0] & 1) !== 0 ? 255 : 0) ]; if (this.vertFlip[0]) { toffset = 15 - toffset; } else { toffset -= 8; } } toffset *= 8; bufferIndex = scan * 256 + x; if (this.horiFlip[0]) { for (i = 7; i >= 0; i--) { if (x >= 0 && x < 256) { if ( bufferIndex >= 0 && bufferIndex < 61440 && this.pixrendered[bufferIndex] !== 0 ) { if (t.pix[toffset + i] !== 0) { this.spr0HitX = bufferIndex % 256; this.spr0HitY = scan; return true; } } } x++; bufferIndex++; } } else { for (i = 0; i < 8; i++) { if (x >= 0 && x < 256) { if ( bufferIndex >= 0 && bufferIndex < 61440 && this.pixrendered[bufferIndex] !== 0 ) { if (t.pix[toffset + i] !== 0) { this.spr0HitX = bufferIndex % 256; this.spr0HitY = scan; return true; } } } x++; bufferIndex++; } } } } return false; }, // This will write to PPU memory, and // update internally buffered data // appropriately. writeMem: function(address, value) { this.vramMem[address] = value; // Update internally buffered data: if (address < 0x2000) { this.vramMem[address] = value; this.patternWrite(address, value); } else if (address >= 0x2000 && address < 0x23c0) { this.nameTableWrite(this.ntable1[0], address - 0x2000, value); } else if (address >= 0x23c0 && address < 0x2400) { this.attribTableWrite(this.ntable1[0], address - 0x23c0, value); } else if (address >= 0x2400 && address < 0x27c0) { this.nameTableWrite(this.ntable1[1], address - 0x2400, value); } else if (address >= 0x27c0 && address < 0x2800) { this.attribTableWrite(this.ntable1[1], address - 0x27c0, value); } else if (address >= 0x2800 && address < 0x2bc0) { this.nameTableWrite(this.ntable1[2], address - 0x2800, value); } else if (address >= 0x2bc0 && address < 0x2c00) { this.attribTableWrite(this.ntable1[2], address - 0x2bc0, value); } else if (address >= 0x2c00 && address < 0x2fc0) { this.nameTableWrite(this.ntable1[3], address - 0x2c00, value); } else if (address >= 0x2fc0 && address < 0x3000) { this.attribTableWrite(this.ntable1[3], address - 0x2fc0, value); } else if (address >= 0x3f00 && address < 0x3f20) { this.updatePalettes(); } }, // Reads data from $3f00 to $f20 // into the two buffered palettes. updatePalettes: function() { var i; for (i = 0; i < 16; i++) { if (this.f_dispType === 0) { this.imgPalette[i] = this.palTable.getEntry( this.vramMem[0x3f00 + i] & 63 ); } else { this.imgPalette[i] = this.palTable.getEntry( this.vramMem[0x3f00 + i] & 32 ); } } for (i = 0; i < 16; i++) { if (this.f_dispType === 0) { this.sprPalette[i] = this.palTable.getEntry( this.vramMem[0x3f10 + i] & 63 ); } else { this.sprPalette[i] = this.palTable.getEntry( this.vramMem[0x3f10 + i] & 32 ); } } }, // Updates the internal pattern // table buffers with this new byte. // In vNES, there is a version of this with 4 arguments which isn't used. patternWrite: function(address, value) { var tileIndex = Math.floor(address / 16); var leftOver = address % 16; if (leftOver < 8) { this.ptTile[tileIndex].setScanline( leftOver, value, this.vramMem[address + 8] ); } else { this.ptTile[tileIndex].setScanline( leftOver - 8, this.vramMem[address - 8], value ); } }, // Updates the internal name table buffers // with this new byte. nameTableWrite: function(index, address, value) { this.nameTable[index].tile[address] = value; // Update Sprite #0 hit: //updateSpr0Hit(); this.checkSprite0(this.scanline - 20); }, // Updates the internal pattern // table buffers with this new attribute // table byte. attribTableWrite: function(index, address, value) { this.nameTable[index].writeAttrib(address, value); }, // Updates the internally buffered sprite // data with this new byte of info. spriteRamWriteUpdate: function(address, value) { var tIndex = Math.floor(address / 4); if (tIndex === 0) { //updateSpr0Hit(); this.checkSprite0(this.scanline - 20); } if (address % 4 === 0) { // Y coordinate this.sprY[tIndex] = value; } else if (address % 4 === 1) { // Tile index this.sprTile[tIndex] = value; } else if (address % 4 === 2) { // Attributes this.vertFlip[tIndex] = (value & 0x80) !== 0; this.horiFlip[tIndex] = (value & 0x40) !== 0; this.bgPriority[tIndex] = (value & 0x20) !== 0; this.sprCol[tIndex] = (value & 3) << 2; } else if (address % 4 === 3) { // X coordinate this.sprX[tIndex] = value; } }, doNMI: function() { // Set VBlank flag: this.setStatusFlag(this.STATUS_VBLANK, true); //nes.getCpu().doNonMaskableInterrupt(); this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); }, isPixelWhite: function(x, y) { this.triggerRendering(); return this.nes.ppu.buffer[(y << 8) + x] === 0xffffff; }, JSON_PROPERTIES: [ // Memory "vramMem", "spriteMem", // Counters "cntFV", "cntV", "cntH", "cntVT", "cntHT", // Registers "regFV", "regV", "regH", "regVT", "regHT", "regFH", "regS", // VRAM addr "vramAddress", "vramTmpAddress", // Control/Status registers "f_nmiOnVblank", "f_spriteSize", "f_bgPatternTable", "f_spPatternTable", "f_addrInc", "f_nTblAddress", "f_color", "f_spVisibility", "f_bgVisibility", "f_spClipping", "f_bgClipping", "f_dispType", // VRAM I/O "vramBufferedReadValue", "firstWrite", // Mirroring "currentMirroring", "vramMirrorTable", "ntable1", // SPR-RAM I/O "sramAddress", // Sprites. Most sprite data is rebuilt from spriteMem "hitSpr0", // Palettes "sprPalette", "imgPalette", // Rendering progression "curX", "scanline", "lastRenderedScanline", "curNt", "scantile", // Used during rendering "attrib", "buffer", "bgbuffer", "pixrendered", // Misc "requestEndFrame", "nmiOk", "dummyCycleToggle", "nmiCounter", "validTileData", "scanlineAlreadyRendered" ], toJSON: function() { var i; var state = utils.toJSON(this); state.nameTable = []; for (i = 0; i < this.nameTable.length; i++) { state.nameTable[i] = this.nameTable[i].toJSON(); } state.ptTile = []; for (i = 0; i < this.ptTile.length; i++) { state.ptTile[i] = this.ptTile[i].toJSON(); } return state; }, fromJSON: function(state) { var i; utils.fromJSON(this, state); for (i = 0; i < this.nameTable.length; i++) { this.nameTable[i].fromJSON(state.nameTable[i]); } for (i = 0; i < this.ptTile.length; i++) { this.ptTile[i].fromJSON(state.ptTile[i]); } // Sprite data: for (i = 0; i < this.spriteMem.length; i++) { this.spriteRamWriteUpdate(i, this.spriteMem[i]); } } }; var NameTable = function(width, height, name) { this.width = width; this.height = height; this.name = name; this.tile = new Array(width * height); this.attrib = new Array(width * height); for (var i = 0; i < width * height; i++) { this.tile[i] = 0; this.attrib[i] = 0; } }; NameTable.prototype = { getTileIndex: function(x, y) { return this.tile[y * this.width + x]; }, getAttrib: function(x, y) { return this.attrib[y * this.width + x]; }, writeAttrib: function(index, value) { var basex = (index % 8) * 4; var basey = Math.floor(index / 8) * 4; var add; var tx, ty; var attindex; for (var sqy = 0; sqy < 2; sqy++) { for (var sqx = 0; sqx < 2; sqx++) { add = (value >> (2 * (sqy * 2 + sqx))) & 3; for (var y = 0; y < 2; y++) { for (var x = 0; x < 2; x++) { tx = basex + sqx * 2 + x; ty = basey + sqy * 2 + y; attindex = ty * this.width + tx; this.attrib[attindex] = (add << 2) & 12; } } } } }, toJSON: function() { return { tile: this.tile, attrib: this.attrib }; }, fromJSON: function(s) { this.tile = s.tile; this.attrib = s.attrib; } }; var PaletteTable = function() { this.curTable = new Array(64); this.emphTable = new Array(8); this.currentEmph = -1; }; PaletteTable.prototype = { reset: function() { this.setEmphasis(0); }, loadNTSCPalette: function() { // prettier-ignore this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000]; this.makeTables(); this.setEmphasis(0); }, loadPALPalette: function() { // prettier-ignore this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000]; this.makeTables(); this.setEmphasis(0); }, makeTables: function() { var r, g, b, col, i, rFactor, gFactor, bFactor; // Calculate a table for each possible emphasis setting: for (var emph = 0; emph < 8; emph++) { // Determine color component factors: rFactor = 1.0; gFactor = 1.0; bFactor = 1.0; if ((emph & 1) !== 0) { rFactor = 0.75; bFactor = 0.75; } if ((emph & 2) !== 0) { rFactor = 0.75; gFactor = 0.75; } if ((emph & 4) !== 0) { gFactor = 0.75; bFactor = 0.75; } this.emphTable[emph] = new Array(64); // Calculate table: for (i = 0; i < 64; i++) { col = this.curTable[i]; r = Math.floor(this.getRed(col) * rFactor); g = Math.floor(this.getGreen(col) * gFactor); b = Math.floor(this.getBlue(col) * bFactor); this.emphTable[emph][i] = this.getRgb(r, g, b); } } }, setEmphasis: function(emph) { if (emph !== this.currentEmph) { this.currentEmph = emph; for (var i = 0; i < 64; i++) { this.curTable[i] = this.emphTable[emph][i]; } } }, getEntry: function(yiq) { return this.curTable[yiq]; }, getRed: function(rgb) { return (rgb >> 16) & 0xff; }, getGreen: function(rgb) { return (rgb >> 8) & 0xff; }, getBlue: function(rgb) { return rgb & 0xff; }, getRgb: function(r, g, b) { return (r << 16) | (g << 8) | b; }, loadDefaultPalette: function() { this.curTable[0] = this.getRgb(117, 117, 117); this.curTable[1] = this.getRgb(39, 27, 143); this.curTable[2] = this.getRgb(0, 0, 171); this.curTable[3] = this.getRgb(71, 0, 159); this.curTable[4] = this.getRgb(143, 0, 119); this.curTable[5] = this.getRgb(171, 0, 19); this.curTable[6] = this.getRgb(167, 0, 0); this.curTable[7] = this.getRgb(127, 11, 0); this.curTable[8] = this.getRgb(67, 47, 0); this.curTable[9] = this.getRgb(0, 71, 0); this.curTable[10] = this.getRgb(0, 81, 0); this.curTable[11] = this.getRgb(0, 63, 23); this.curTable[12] = this.getRgb(27, 63, 95); this.curTable[13] = this.getRgb(0, 0, 0); this.curTable[14] = this.getRgb(0, 0, 0); this.curTable[15] = this.getRgb(0, 0, 0); this.curTable[16] = this.getRgb(188, 188, 188); this.curTable[17] = this.getRgb(0, 115, 239); this.curTable[18] = this.getRgb(35, 59, 239); this.curTable[19] = this.getRgb(131, 0, 243); this.curTable[20] = this.getRgb(191, 0, 191); this.curTable[21] = this.getRgb(231, 0, 91); this.curTable[22] = this.getRgb(219, 43, 0); this.curTable[23] = this.getRgb(203, 79, 15); this.curTable[24] = this.getRgb(139, 115, 0); this.curTable[25] = this.getRgb(0, 151, 0); this.curTable[26] = this.getRgb(0, 171, 0); this.curTable[27] = this.getRgb(0, 147, 59); this.curTable[28] = this.getRgb(0, 131, 139); this.curTable[29] = this.getRgb(0, 0, 0); this.curTable[30] = this.getRgb(0, 0, 0); this.curTable[31] = this.getRgb(0, 0, 0); this.curTable[32] = this.getRgb(255, 255, 255); this.curTable[33] = this.getRgb(63, 191, 255); this.curTable[34] = this.getRgb(95, 151, 255); this.curTable[35] = this.getRgb(167, 139, 253); this.curTable[36] = this.getRgb(247, 123, 255); this.curTable[37] = this.getRgb(255, 119, 183); this.curTable[38] = this.getRgb(255, 119, 99); this.curTable[39] = this.getRgb(255, 155, 59); this.curTable[40] = this.getRgb(243, 191, 63); this.curTable[41] = this.getRgb(131, 211, 19); this.curTable[42] = this.getRgb(79, 223, 75); this.curTable[43] = this.getRgb(88, 248, 152); this.curTable[44] = this.getRgb(0, 235, 219); this.curTable[45] = this.getRgb(0, 0, 0); this.curTable[46] = this.getRgb(0, 0, 0); this.curTable[47] = this.getRgb(0, 0, 0); this.curTable[48] = this.getRgb(255, 255, 255); this.curTable[49] = this.getRgb(171, 231, 255); this.curTable[50] = this.getRgb(199, 215, 255); this.curTable[51] = this.getRgb(215, 203, 255); this.curTable[52] = this.getRgb(255, 199, 255); this.curTable[53] = this.getRgb(255, 199, 219); this.curTable[54] = this.getRgb(255, 191, 179); this.curTable[55] = this.getRgb(255, 219, 171); this.curTable[56] = this.getRgb(255, 231, 163); this.curTable[57] = this.getRgb(227, 255, 163); this.curTable[58] = this.getRgb(171, 243, 191); this.curTable[59] = this.getRgb(179, 255, 207); this.curTable[60] = this.getRgb(159, 255, 243); this.curTable[61] = this.getRgb(0, 0, 0); this.curTable[62] = this.getRgb(0, 0, 0); this.curTable[63] = this.getRgb(0, 0, 0); this.makeTables(); this.setEmphasis(0); } }; module.exports = PPU;