/* JSNES, based on Jamie Sanders' vNES Copyright (C) 2010 Ben Firshman This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ JSNES.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.prevBuffer = 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(); }; JSNES.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= 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= 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; } } } if (this.nes.opts.showDisplay) { this.nes.ui.writeFrame(buffer, this.prevBuffer); } }, 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<>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= 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 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]; tpix = t.pix; att = attrib[tile]; }else { // Fetch data: t = ptTile[baseTile+nameTable[this.curNt].getTileIndex(this.cntHT,this.cntVT)]; 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) { 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+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+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; var col; var bgPri; 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]; col = this.sprCol[0]; bgPri = this.bgPriority[0]; 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; col = this.sprCol[0]; bgPri = this.bgPriority[0]; 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); }, 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 = JSNES.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; JSNES.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]); } } }; JSNES.PPU.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); }; JSNES.PPU.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[ty*this.width+tx] = (add<<2)&12; } } } } }, toJSON: function() { return { 'tile': this.tile, 'attrib': this.attrib }; }, fromJSON: function(s) { this.tile = s.tile; this.attrib = s.attrib; } }; JSNES.PPU.PaletteTable = function() { this.curTable = new Array(64); this.emphTable = new Array(8); this.currentEmph = -1; }; JSNES.PPU.PaletteTable.prototype = { reset: function() { this.setEmphasis(0); }, loadNTSCPalette: function() { 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() { 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); } }; JSNES.PPU.Tile = function() { // Tile data: this.pix = new Array(64); this.fbIndex = null; this.tIndex = null; this.x = null; this.y = null; this.w = null; this.h = null; this.incX = null; this.incY = null; this.palIndex = null; this.tpri = null; this.c = null; this.initialized = false; this.opaque = new Array(8); }; JSNES.PPU.Tile.prototype = { setBuffer: function(scanline){ for (this.y=0;this.y<8;this.y++) { this.setScanline(this.y,scanline[this.y],scanline[this.y+8]); } }, setScanline: function(sline, b1, b2){ this.initialized = true; this.tIndex = sline<<3; for (this.x = 0; this.x < 8; this.x++) { this.pix[this.tIndex + this.x] = ((b1 >> (7 - this.x)) & 1) + (((b2 >> (7 - this.x)) & 1) << 1); if(this.pix[this.tIndex+this.x] === 0) { this.opaque[sline] = false; } } }, render: function(buffer, srcx1, srcy1, srcx2, srcy2, dx, dy, palAdd, palette, flipHorizontal, flipVertical, pri, priTable) { if (dx<-7 || dx>=256 || dy<-7 || dy>=240) { return; } this.w=srcx2-srcx1; this.h=srcy2-srcy1; if (dx<0) { srcx1-=dx; } if (dx+srcx2>=256) { srcx2=256-dx; } if (dy<0) { srcy1-=dy; } if (dy+srcy2>=240) { srcy2=240-dy; } if (!flipHorizontal && !flipVertical) { this.fbIndex = (dy<<8)+dx; this.tIndex = 0; for (this.y=0;this.y<8;this.y++) { for (this.x=0;this.x<8;this.x++) { if (this.x>=srcx1 && this.x=srcy1 && this.y=srcx1 && this.x=srcy1 && this.y=srcx1 && this.x=srcy1 && this.y=srcx1 && this.x=srcy1 && this.y