1754 lines
49 KiB
JavaScript
1754 lines
49 KiB
JavaScript
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;
|