'use strict'; /* * js99'er - TI-99/4A emulator written in JavaScript * * Created 2014 by Rasmus Moustgaard * * TMS9918A VDP emulation. * * https://github.com/Rasmus-M/Js99er * GNU General Public License v2.0 */ import { hex, lpad } from "../util"; enum TMS9918A_Mode { GRAPHICS = 0, TEXT = 1, BITMAP = 2, MULTICOLOR = 3, BITMAP_TEXT = 4, BITMAP_MULTICOLOR = 5, ILLEGAL = 6, }; /** * @constructor */ export class TMS9918A { canvas; cru : { setVDPInterrupt: (b:boolean) => void }; enableFlicker : boolean; ram = new Uint8Array(16384); // VDP RAM registers = new Uint8Array(8); spriteBuffer = new Uint8Array(256); addressRegister : number; statusRegister : number; palette : [number, number, number][]; latch : boolean; prefetchByte : number; displayOn = null; interruptsOn = null; screenMode : number; bitmapMode : boolean; textMode : boolean; colorTable : number; nameTable : number; charPatternTable : number; spriteAttributeTable : number; spritePatternTable : number; colorTableMask : number; patternTableMask : number; ramMask : number; fgColor : number; bgColor : number; flicker : boolean; redrawRequired : boolean; canvasContext; imageData; width : number; height : number; constructor(canvas, cru, enableFlicker:boolean) { this.canvas = canvas; this.cru = cru; this.enableFlicker = enableFlicker; this.palette = [ [0, 0, 0], [0, 0, 0], [33, 200, 66], [94, 220, 120], [84, 85, 237], [125, 118, 252], [212, 82, 77], [66, 235, 245], [252, 85, 84], [255, 121, 120], [212, 193, 84], [230, 206, 128], [33, 176, 59], [201, 91, 186], [204, 204, 204], [255, 255, 255] ]; this.canvasContext = this.canvas.getContext("2d"); this.reset(); } reset() { var i; for (i = 0; i < this.ram.length; i++) { this.ram[i] = 0; } for (i = 0; i < this.registers.length; i++) { this.registers[i] = 0; } this.addressRegister = 0; this.statusRegister = 0; this.prefetchByte = 0; this.latch = false; this.displayOn = false; this.interruptsOn = false; this.screenMode = TMS9918A_Mode.GRAPHICS; this.bitmapMode = false; this.textMode = false; this.colorTable = 0; this.nameTable = 0; this.charPatternTable = 0; this.spriteAttributeTable = 0; this.spritePatternTable = 0; this.colorTableMask = 0x3FFF; this.patternTableMask = 0x3FFF; this.ramMask = 0x3FFF; this.fgColor = 0; this.bgColor = 0; this.flicker = this.enableFlicker; this.redrawRequired = true; this.canvas.width = 304; this.canvas.height = 240; this.canvasContext.fillStyle = 'rgba(' + this.palette[7].join(',') + ',1.0)'; this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height); // Build the array containing the canvas bitmap (256 * 192 * 4 bytes (r,g,b,a) format each pixel) this.imageData = this.canvasContext.getImageData(0, 0, this.canvas.width, this.canvas.height); this.width = this.canvas.width; this.height = this.canvas.height; } drawFrame(timestamp:number) { if (this.redrawRequired) { for (var y = 0; y < this.height; y++) { this.drawScanline(y); } this.updateCanvas(); this.redrawRequired = false; } } initFrame(timestamp:number) { } drawScanline(y:number) { var imageData = this.imageData.data, width = this.width, imageDataAddr = (y * width) << 2, screenMode = this.screenMode, textMode = this.textMode, bitmapMode = this.bitmapMode, drawWidth = !textMode ? 256 : 240, drawHeight = 192, hBorder = (width - drawWidth) >> 1, vBorder = (this.height - drawHeight) >> 1, fgColor = this.fgColor, bgColor = this.bgColor, ram = this.ram, nameTable = this.nameTable, colorTable = this.colorTable, charPatternTable = this.charPatternTable, colorTableMask = this.colorTableMask, patternTableMask = this.patternTableMask, spriteAttributeTable = this.spriteAttributeTable, spritePatternTable = this.spritePatternTable, spriteSize = (this.registers[1] & 0x2) !== 0, spriteMagnify = this.registers[1] & 0x1, spriteDimension = (spriteSize ? 16 : 8) << (spriteMagnify ? 1 : 0), maxSpritesOnLine = this.flicker ? 4 : 32, palette = this.palette, collision = false, fifthSprite = false, fifthSpriteIndex = 31, x, color, rgbColor, name, tableOffset, colorByte, patternByte; if (y >= vBorder && y < vBorder + drawHeight && this.displayOn) { var y1 = y - vBorder; // Pre-process sprites if (!textMode) { var spriteBuffer = this.spriteBuffer; spriteBuffer.fill(0); var spritesOnLine = 0; var endMarkerFound = false; var spriteAttributeAddr = spriteAttributeTable; var s; for (s = 0; s < 32 && spritesOnLine <= maxSpritesOnLine && !endMarkerFound; s++) { var sy = ram[spriteAttributeAddr]; if (sy !== 0xD0) { if (sy > 0xD0) { sy -= 256; } sy++; var sy1 = sy + spriteDimension; var y2 = -1; if (s < 8 || !bitmapMode) { if (y1 >= sy && y1 < sy1) { y2 = y1; } } else { // Emulate sprite duplication bug var yMasked = y1 & (((this.registers[4] & 0x03) << 6) | 0x3F); if (yMasked >= sy && yMasked < sy1) { y2 = yMasked; } else if (y1 >= 64 && y1 < 128 && y1 >= sy && y1 < sy1) { y2 = y1; } } if (y2 !== -1) { if (spritesOnLine < maxSpritesOnLine) { var sx = ram[spriteAttributeAddr + 1]; var sPatternNo = ram[spriteAttributeAddr + 2] & (spriteSize ? 0xFC : 0xFF); var sColor = ram[spriteAttributeAddr + 3] & 0x0F; if ((ram[spriteAttributeAddr + 3] & 0x80) !== 0) { sx -= 32; } var sLine = (y2 - sy) >> spriteMagnify; var sPatternBase = spritePatternTable + (sPatternNo << 3) + sLine; for (var sx1 = 0; sx1 < spriteDimension; sx1++) { var sx2 = sx + sx1; if (sx2 >= 0 && sx2 < drawWidth) { var sx3 = sx1 >> spriteMagnify; var sPatternByte = ram[sPatternBase + (sx3 >= 8 ? 16 : 0)]; if ((sPatternByte & (0x80 >> (sx3 & 0x07))) !== 0) { if (spriteBuffer[sx2] === 0) { spriteBuffer[sx2] = sColor + 1; } else { collision = true; } } } } } spritesOnLine++; } spriteAttributeAddr += 4; } else { endMarkerFound = true; } } if (spritesOnLine > 4) { fifthSprite = true; fifthSpriteIndex = s; } } // Draw var rowOffset = !textMode ? (y1 >> 3) << 5 : (y1 >> 3) * 40; var lineOffset = y1 & 7; for (x = 0; x < width; x++) { if (x >= hBorder && x < hBorder + drawWidth) { var x1 = x - hBorder; // Tiles switch (screenMode) { case TMS9918A_Mode.GRAPHICS: name = ram[nameTable + rowOffset + (x1 >> 3)]; colorByte = ram[colorTable + (name >> 3)]; patternByte = ram[charPatternTable + (name << 3) + lineOffset]; color = (patternByte & (0x80 >> (x1 & 7))) !== 0 ? (colorByte & 0xF0) >> 4 : colorByte & 0x0F; break; case TMS9918A_Mode.BITMAP: name = ram[nameTable + rowOffset + (x1 >> 3)]; tableOffset = ((y1 & 0xC0) << 5) + (name << 3); colorByte = ram[colorTable + (tableOffset & colorTableMask) + lineOffset]; patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset]; color = (patternByte & (0x80 >> (x1 & 7))) !== 0 ? (colorByte & 0xF0) >> 4 : colorByte & 0x0F; break; case TMS9918A_Mode.MULTICOLOR: name = ram[nameTable + rowOffset + (x1 >> 3)]; lineOffset = (y1 & 0x1C) >> 2; patternByte = ram[charPatternTable + (name << 3) + lineOffset]; color = (x1 & 4) === 0 ? (patternByte & 0xF0) >> 4 : patternByte & 0x0F; break; case TMS9918A_Mode.TEXT: name = ram[nameTable + rowOffset + Math.floor(x1 / 6)]; patternByte = ram[charPatternTable + (name << 3) + lineOffset]; color = (patternByte & (0x80 >> (x1 % 6))) !== 0 ? fgColor : bgColor; break; case TMS9918A_Mode.BITMAP_TEXT: name = ram[nameTable + rowOffset + Math.floor(x1 / 6)]; tableOffset = ((y1 & 0xC0) << 5) + (name << 3); patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset]; color = (patternByte & (0x80 >> (x1 % 6))) !== 0 ? fgColor : bgColor; break; case TMS9918A_Mode.BITMAP_MULTICOLOR: name = ram[nameTable + rowOffset + (x1 >> 3)]; lineOffset = (y1 & 0x1C) >> 2; tableOffset = ((y1 & 0xC0) << 5) + (name << 3); patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset]; color = (x1 & 4) === 0 ? (patternByte & 0xF0) >> 4 : patternByte & 0x0F; break; case TMS9918A_Mode.ILLEGAL: color = (x1 & 4) === 0 ? fgColor : bgColor; break; } if (color === 0) { color = bgColor; } // Sprites if (!textMode) { var spriteColor = spriteBuffer[x1] - 1; if (spriteColor > 0) { color = spriteColor; } } } else { color = bgColor; } rgbColor = palette[color]; imageData[imageDataAddr++] = rgbColor[0]; // R imageData[imageDataAddr++] = rgbColor[1]; // G imageData[imageDataAddr++] = rgbColor[2]; // B imageDataAddr++; // Skip alpha } } // Top/bottom border else { rgbColor = palette[bgColor]; for (x = 0; x < width; x++) { imageData[imageDataAddr++] = rgbColor[0]; // R imageData[imageDataAddr++] = rgbColor[1]; // G imageData[imageDataAddr++] = rgbColor[2]; // B imageDataAddr++; // Skip alpha } } if (y === vBorder + drawHeight) { this.statusRegister |= 0x80; if (this.interruptsOn) { this.cru.setVDPInterrupt(true); } } if (collision) { this.statusRegister |= 0x20; } if ((this.statusRegister & 0x40) === 0) { this.statusRegister |= fifthSpriteIndex; } if (fifthSprite) { this.statusRegister |= 0x40; } } updateCanvas() { this.canvasContext.putImageData(this.imageData, 0, 0); } setReadAddress(i:number) { this.addressRegister = ((i & 0x3f) << 8) | (this.addressRegister & 0x00FF); this.prefetchByte = this.ram[this.addressRegister++]; this.addressRegister &= 0x3FFF; } setWriteAddress(i:number) { this.addressRegister = ((i & 0x3f) << 8) | (this.addressRegister & 0x00FF); } setVDPWriteRegister(i:number) { var regmask = this.registers.length-1; this.registers[i & regmask] = this.addressRegister & 0x00FF; switch (i & regmask) { // Mode case 0: this.updateMode(this.registers[0], this.registers[1]); break; case 1: this.ramMask = (this.registers[1] & 0x80) !== 0 ? 0x3FFF : 0x1FFF; this.displayOn = (this.registers[1] & 0x40) !== 0; this.interruptsOn = (this.registers[1] & 0x20) !== 0; this.updateMode(this.registers[0], this.registers[1]); break; // Name table case 2: this.nameTable = (this.registers[2] & 0xf) << 10; break; // Color table case 3: if (this.bitmapMode) { this.colorTable = (this.registers[3] & 0x80) << 6; } else { this.colorTable = this.registers[3] << 6; } this.updateTableMasks(); break; // Pattern table case 4: if (this.bitmapMode) { this.charPatternTable = (this.registers[4] & 0x4) << 11; } else { this.charPatternTable = (this.registers[4] & 0x7) << 11; } this.updateTableMasks(); break; // Sprite attribute table case 5: this.spriteAttributeTable = (this.registers[5] & 0x7f) << 7; break; // Sprite pattern table case 6: this.spritePatternTable = (this.registers[6] & 0x7) << 11; break; // Background case 7: this.fgColor = (this.registers[7] & 0xf0) >> 4; this.bgColor = this.registers[7] & 0x0f; break; } // this.logRegisters(); // this.log.info("Name table: " + this.nameTable.toHexWord()); // this.log.info("Pattern table: " + this.charPatternTable.toHexWord()); } setVDPWriteCommand3(i:number) { this.setVDPWriteRegister(i); } writeAddress(i:number) { if (!this.latch) { this.addressRegister = (this.addressRegister & 0xFF00) | i; } else { switch ((i & 0xc0) >> 6) { // Set read address case 0: this.setReadAddress(i); break; // Set write address case 1: this.setWriteAddress(i); break; // Write register case 2: this.setVDPWriteRegister(i); break; case 3: this.setVDPWriteCommand3(i); break; } this.redrawRequired = true; } this.latch = !this.latch; } updateMode(reg0:number, reg1:number) { this.bitmapMode = (reg0 & 0x02) !== 0; this.textMode = (reg1 & 0x10) !== 0; // Check bitmap mode bit, not text or multicolor if (this.bitmapMode) { switch ((reg1 & 0x18) >> 3) { case 0: // Bitmap mode this.screenMode = TMS9918A_Mode.BITMAP; break; case 1: // Multicolor mode this.screenMode = TMS9918A_Mode.BITMAP_MULTICOLOR; break; case 2: // Text mode this.screenMode = TMS9918A_Mode.BITMAP_TEXT; break; case 3: // Illegal this.screenMode = TMS9918A_Mode.ILLEGAL; break; } } else { switch ((reg1 & 0x18) >> 3) { case 0: // Graphics mode 0 this.screenMode = TMS9918A_Mode.GRAPHICS; break; case 1: // Multicolor mode this.screenMode = TMS9918A_Mode.MULTICOLOR; break; case 2: // Text mode this.screenMode = TMS9918A_Mode.TEXT; break; case 3: // Illegal this.screenMode = TMS9918A_Mode.ILLEGAL; break; } } if (this.bitmapMode) { this.colorTable = (this.registers[3] & 0x80) << 6; this.charPatternTable = (this.registers[4] & 0x4) << 11; this.updateTableMasks(); } else { this.colorTable = this.registers[3] << 6; this.charPatternTable = (this.registers[4] & 0x7) << 11; } this.nameTable = (this.registers[2] & 0xf) << 10; this.spriteAttributeTable = (this.registers[5] & 0x7f) << 7; this.spritePatternTable = (this.registers[6] & 0x7) << 11; } updateTableMasks() { if (this.screenMode === TMS9918A_Mode.BITMAP) { this.colorTableMask = ((this.registers[3] & 0x7F) << 6) | 0x3F; // 000CCCCCCC111111 this.patternTableMask = ((this.registers[4] & 0x03) << 11) | (this.colorTableMask & 0x7FF); // 000PPCCCCC111111 // this.log.info("colorTableMask:" + this.colorTableMask); // this.log.info("patternTableMask:" + this.patternTableMask); } else if (this.screenMode === TMS9918A_Mode.BITMAP_TEXT || this.screenMode === TMS9918A_Mode.BITMAP_MULTICOLOR) { this.colorTableMask = this.ramMask; this.patternTableMask = ((this.registers[4] & 0x03) << 11) | 0x7FF; // 000PP11111111111 } else { this.colorTableMask = this.ramMask; this.patternTableMask = this.ramMask; } } writeData(i:number) { this.ram[this.addressRegister++] = i; this.addressRegister &= this.ramMask; this.redrawRequired = true; } readStatus() : number { var i = this.statusRegister; this.statusRegister = 0x1F; if (this.interruptsOn) { this.cru.setVDPInterrupt(false); } this.latch = false; return i; } readData() : number { var i = this.prefetchByte; this.prefetchByte = this.ram[this.addressRegister++]; this.addressRegister &= this.ramMask; return i; } getRAM() : Uint8Array { return this.ram; } colorTableSize() : number { if (this.screenMode === TMS9918A_Mode.GRAPHICS) { return 0x20; } else if (this.screenMode === TMS9918A_Mode.BITMAP) { return Math.min(this.colorTableMask + 1, 0x1800); } else { return 0; } } patternTableSize() : number { if (this.bitmapMode) { return Math.min(this.patternTableMask + 1, 0x1800); } else { return 0x800; } } getRegsString() : string { const w = 20; var s = "Registers:"; for (var i = 0; i < this.registers.length; i++) { s += " " + hex(this.registers[i],2); } s += "\n\n"; var tables : [string,number,number][] = [ ["Pattern Table", this.charPatternTable, this.patternTableSize()], ["Image Table", this.nameTable, 0x300], ["Color Table", this.colorTable, this.colorTableSize()], ["Sprite Patterns", this.spritePatternTable, 64*32], ["Sprite Attributes", this.spriteAttributeTable, 4*32], ]; for (var row of tables) { if (row[2] > 0) s += lpad(row[0], w) + ": $" + hex(row[1],4) + " - $" + hex(row[1]+row[2]-1,4) + "\n"; } s += lpad("Address Register",w) + ": $" + hex(this.addressRegister,4) + "\n"; s += lpad("Screen Mode",w) + ": " + this.screenMode + "\n"; s += lpad("Display On",w) + ": " + this.displayOn + "\n"; if (this.ramMask != 0x3fff) s += lpad("RAM Mask",w) + ": $" + hex(this.ramMask) + "\n"; return s; } hexView(start:number, length:number, anchorAddr:number) : {text,lineCount,anchorLine} { var text = ""; var anchorLine = null; var addr = start; var line = 0; for (var i = 0; i < length && addr < 0x4000; addr++, i++) { if ((i & 0x000F) === 0) { text += "\n" + hex(addr,4) + ":"; line++; } text += " "; if (anchorAddr && anchorAddr === addr) { anchorLine = line; } var hx = this.ram[addr].toString(16).toUpperCase(); if (hx.length === 1) { text += "0"; } text += hx; } return {text: text.substr(1), lineCount: line, anchorLine: anchorLine - 1}; } getWord(addr:number) : number { return addr < 0x4000 ? this.ram[addr] << 8 | this.ram[addr+1] : 0; } getCharAt(x:number, y:number) : number { x -= 24; y -= 24; if (!this.textMode) { return this.ram[this.nameTable + Math.floor(x / 8) + Math.floor(y / 8) * 32]; } else { return this.ram[this.nameTable + Math.floor((x - 8) / 6) + Math.floor(y / 8) * 40]; } } setFlicker(value:boolean) { this.flicker = value; this.enableFlicker = value; } getState() { return { ram: this.ram.slice(0), registers: this.registers.slice(0), addressRegister: this.addressRegister, statusRegister: this.statusRegister, latch: this.latch, prefetchByte: this.prefetchByte, displayOn: this.displayOn, interruptsOn: this.interruptsOn, screenMode: this.screenMode, bitmapMode: this.bitmapMode, textMode: this.textMode, colorTable: this.colorTable, nameTable: this.nameTable, charPatternTable: this.charPatternTable, spriteAttributeTable: this.spriteAttributeTable, spritePatternTable: this.spritePatternTable, colorTableMask: this.colorTableMask, patternTableMask: this.patternTableMask, ramMask: this.ramMask, fgColor: this.fgColor, bgColor: this.bgColor, flicker: this.flicker }; } restoreState(state) { this.ram.set(state.ram); this.registers.set(state.registers); this.addressRegister = state.addressRegister; this.statusRegister = state.statusRegister; this.latch = state.latch; this.prefetchByte = state.prefetchByte; this.displayOn = state.displayOn; this.interruptsOn = state.interruptsOn; this.screenMode = state.screenMode; this.bitmapMode = state.bitmapMode; this.textMode = state.textMode; this.colorTable = state.colorTable; this.nameTable = state.nameTable; this.charPatternTable = state.charPatternTable; this.spriteAttributeTable = state.spriteAttributeTable; this.spritePatternTable = state.spritePatternTable; this.colorTableMask = state.colorTableMask; this.patternTableMask = state.patternTableMask; this.ramMask = state.ramMask; this.fgColor = state.fgColor; this.bgColor = state.bgColor; this.flicker = state.flicker; this.redrawRequired = true; } }; export class SMSVDP extends TMS9918A { writeToCRAM : boolean; cram = new Uint8Array(32); // color RAM registers = new Uint8Array(16); // 8 more registers setReadAddress(i:number) { super.setReadAddress(i); this.writeToCRAM = false; } setWriteAddress(i:number) { super.setWriteAddress(i); this.writeToCRAM = false; } setVDPWriteRegister(i:number) { super.setVDPWriteRegister(i); this.writeToCRAM = false; } setVDPWriteCommand3(i:number) { this.writeToCRAM = true; } writeData(i:number) { if (this.writeToCRAM) { this.cram[this.addressRegister++ & (this.cram.length-1)] = i; this.addressRegister &= this.ramMask; this.redrawRequired = true; } else { super.writeData(i); } } getState() { var state = super.getState(); state['cram'] = this.cram.slice(0); return state; } restoreState(state) { super.restoreState(state); this.cram.set(state.cram); } };