From 51b6f0025c8c923551d04ff937c8a43cf2f88d7c Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 28 Aug 2022 17:44:05 -0500 Subject: [PATCH] williams: started porting to Machine --- src/common/baseplatform.ts | 33 ++- src/common/cpu/6809.ts | 3 +- src/machine/williams.ts | 398 +++++++++++++++++++++++++++++++++++++ src/platform/williams.ts | 23 ++- 4 files changed, 452 insertions(+), 5 deletions(-) create mode 100644 src/machine/williams.ts diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index 5d471230..31fac491 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -671,7 +671,7 @@ export abstract class Base6809Platform extends BaseZ80Platform { //TODO: how to get stack_end? -export function dumpStackToString(platform:Platform, mem:Uint8Array|number[], start:number, end:number, sp:number, jsrop:number) : string { +export function dumpStackToString(platform:Platform, mem:Uint8Array|number[], start:number, end:number, sp:number, jsrop:number, bigendian?:boolean) : string { var s = ""; var nraw = 0; //s = dumpRAM(mem.slice(start,start+end+1), start, end-start+1); @@ -684,6 +684,7 @@ export function dumpStackToString(platform:Platform, mem:Uint8Array|number[], st // see if there's a JSR on the stack here // TODO: make work with roms and memory maps var addr = read(sp) + read(sp+1)*256; + if (bigendian) { addr = ((addr & 0xff) << 8) | ((addr & 0xff00) >> 8) } var jsrofs = jsrop==0x20 ? -2 : -3; // 6502 vs Z80 var opcode = read(addr + jsrofs); // might be out of bounds if (opcode == jsrop) { // JSR @@ -970,7 +971,6 @@ export abstract class BaseZ80MachinePlatform extends BaseMach var start = sp & 0xff00; var end = start + 0xff; if (sp == 0) sp = 0x10000; - console.log(sp,start,end); return dumpStackToString(this, [], start, end, sp, 0xcd); } default: return isDebuggable(this.machine) && this.machine.getDebugInfo(category, state); @@ -982,6 +982,35 @@ export abstract class BaseZ80MachinePlatform extends BaseMach } +export abstract class Base6809MachinePlatform extends BaseMachinePlatform { + + getToolForFilename = getToolForFilename_6809; + + getDebugCategories() { + if (isDebuggable(this.machine)) + return this.machine.getDebugCategories(); + else + return ['CPU','Stack']; + } + getDebugInfo(category:string, state:EmuState) : string { + switch (category) { + case 'CPU': return cpuStateToLongString_6809(state.c); + case 'Stack': { + var sp = (state.c.SP-1) & 0xffff; + var start = sp & 0xff00; + var end = start + 0xff; + if (sp == 0) sp = 0x10000; + return dumpStackToString(this, [], start, end, sp, 0x17, true); + } + default: return super.getDebugInfo(category, state); + } + } + disassemble(pc:number, read:(addr:number)=>number) : DisasmLine { + // TODO: don't create new CPU + return Object.create(CPU6809()).disasm(read(pc), read(pc+1), read(pc+2), read(pc+3), read(pc+4), pc); + } +} + /// class SerialIOVisualizer { diff --git a/src/common/cpu/6809.ts b/src/common/cpu/6809.ts index e7ab7200..8bb123e1 100644 --- a/src/common/cpu/6809.ts +++ b/src/common/cpu/6809.ts @@ -2456,7 +2456,8 @@ return { } return f; }, - disasm: disasm + disasm: disasm, + isStable: function() { return true; } }; }; diff --git a/src/machine/williams.ts b/src/machine/williams.ts new file mode 100644 index 00000000..e53ea202 --- /dev/null +++ b/src/machine/williams.ts @@ -0,0 +1,398 @@ +import { hex } from "chroma-js"; +import { MasterAudio, WorkerSoundChannel } from "../common/audio"; +import { MemoryBus } from "../common/baseplatform"; +import { CPU6809 } from "../common/cpu/6809"; +import { BasicScanlineMachine } from "../common/devices"; +import { Keys, makeKeycodeMap, newAddressDecoder, newKeyboardHandler, padBytes } from "../common/emu"; + +const INITIAL_WATCHDOG = 8; +const SCREEN_HEIGHT = 304; + +export class WilliamsMachine extends BasicScanlineMachine { + + xtal = 12000000; + cpuFrequency = this.xtal / 3 / 4; + //cpuCyclesPerLine = 64; + cpuCyclesPerLine = 54; // TODO: becuse we swapped width and height + canvasWidth = 256; + numTotalScanlines = 304; + numVisibleScanlines = 304; + defaultROMSize = 0xc000; + rotate = -90; + sampleRate = 1; + + cpu; + membus: MemoryBus; + ram = new Uint8Array(0xc000); + nvram = new Uint8Array(0x400); + rom = new Uint8Array(0xc000); + portsel = 0; + banksel = 0; + watchdog_counter = 0; + watchdog_enabled = false; + pia6821 = new Uint8Array(8); + blitregs = new Uint8Array(8); + + worker: Worker; + master; + audioadapter; + + palette = new Uint32Array(16); + screenNeedsRefresh = false; + displayPCs; + cpuScale = 1; + waitCycles = 0; + + constructor(public isDefender: boolean) { + super(); + this.palette.fill(0xff000000); + this.initBus(isDefender); + this.initInputs(isDefender); + this.initAudio(); + this.initCPU(); + } + + initInputs(isDefender: boolean) { + var DEFENDER_KEYCODE_MAP = makeKeycodeMap([ + [Keys.A, 4, 0x1], + [Keys.RIGHT, 4, 0x2], + [Keys.B, 4, 0x4], + [Keys.VK_X, 4, 0x8], + [Keys.P2_START, 4, 0x10], + [Keys.START, 4, 0x20], + [Keys.LEFT, 4, 0x40], + [Keys.DOWN, 4, 0x80], + [Keys.UP, 6, 0x1], + [Keys.SELECT, 0, 0x4], + [Keys.VK_7, 0, 0x1], + [Keys.VK_8, 0, 0x2], + [Keys.VK_9, 0, 0x8], + ]); + + var ROBOTRON_KEYCODE_MAP = makeKeycodeMap([ + [Keys.P2_UP, 0, 0x1], + [Keys.P2_DOWN, 0, 0x2], + [Keys.P2_LEFT, 0, 0x4], + [Keys.P2_RIGHT, 0, 0x8], + [Keys.START, 0, 0x10], + [Keys.P2_START, 0, 0x20], + [Keys.UP, 0, 0x40], + [Keys.DOWN, 0, 0x80], + [Keys.LEFT, 2, 0x1], + [Keys.RIGHT, 2, 0x2], + [Keys.VK_7, 4, 0x1], + [Keys.VK_8, 4, 0x2], + [Keys.VK_6, 4, 0x4], + [Keys.VK_9, 4, 0x8], + [Keys.SELECT, 4, 0x10], + ]); + + var KEYCODE_MAP = isDefender ? DEFENDER_KEYCODE_MAP : ROBOTRON_KEYCODE_MAP; + //this.inputs.set(this.pia6821); + this.handler = newKeyboardHandler(this.pia6821, KEYCODE_MAP); + } + + initBus(isDefender: boolean) { + var ioread_defender = newAddressDecoder([ + [0x400, 0x5ff, 0x1ff, (a) => { return this.nvram[a]; }], + [0x800, 0x800, 0, (a) => { return this.scanline; }], + [0xc00, 0xc07, 0x7, (a) => { return this.pia6821[a]; }], + [0x0, 0xfff, 0, (a) => { /*console.log('ioread',hex(a));*/ }], + ]); + + var iowrite_defender = newAddressDecoder([ + [0x0, 0xf, 0xf, this.setPalette.bind(this)], + [0x3fc, 0x3ff, 0, (a, v) => { if (v == 0x38) this.watchdog_counter = INITIAL_WATCHDOG; this.watchdog_enabled = true; }], + [0x400, 0x5ff, 0x1ff, (a, v) => { this.nvram[a] = v; }], + [0xc02, 0xc02, 0x1, (a, v) => { if (this.worker) this.worker.postMessage({ command: v & 0x3f }); }], + [0xc00, 0xc07, 0x7, (a, v) => { this.pia6821[a] = v; }], + [0x0, 0xfff, 0, (a, v) => { /* console.log('iowrite', hex(a), hex(v)); */ }], + ]); + + var memread_defender = newAddressDecoder([ + [0x0000, 0xbfff, 0xffff, (a) => { return this.ram[a]; }], + [0xc000, 0xcfff, 0x0fff, (a) => { + switch (this.banksel) { + case 0: return ioread_defender(a); + case 1: return this.rom[a + 0x3000]; + case 2: return this.rom[a + 0x4000]; + case 3: return this.rom[a + 0x5000]; + case 7: return this.rom[a + 0x6000]; + default: return 0; // TODO: error light + } + }], + [0xd000, 0xffff, 0xffff, (a) => { return this.rom ? this.rom[a - 0xd000] : 0; }], + ]); + + var memwrite_defender = newAddressDecoder([ + [0x0000, 0x97ff, 0, this.write_display_byte.bind(this)], + [0x9800, 0xbfff, 0, (a, v) => { this.ram[a] = v; }], + [0xc000, 0xcfff, 0x0fff, iowrite_defender.bind(this)], + [0xd000, 0xdfff, 0, (a, v) => { this.banksel = v & 0x7; }], + [0, 0xffff, 0, (a, v) => { /* console.log(hex(a), hex(v)); */ }], + ]); + + // Robotron, Joust, Bubbles, Stargate + + var ioread_robotron = newAddressDecoder([ + [0x804, 0x807, 0x3, (a) => { return this.pia6821[a]; }], + [0x80c, 0x80f, 0x3, (a) => { return this.pia6821[a + 4]; }], + [0xb00, 0xbff, 0, (a) => { return this.scanline; }], + [0xc00, 0xfff, 0x3ff, (a) => { return this.nvram[a]; }], + [0x0, 0xfff, 0, (a) => { /* console.log('ioread',hex(a)); */ }], + ]); + + var iowrite_robotron = newAddressDecoder([ + [0x0, 0xf, 0xf, this.setPalette.bind(this)], + [0x80c, 0x80c, 0xf, (a, v) => { if (this.worker) this.worker.postMessage({ command: v }); }], + //[0x804, 0x807, 0x3, function(a,v) { console.log('iowrite',a); }], // TODO: sound + //[0x80c, 0x80f, 0x3, function(a,v) { console.log('iowrite',a+4); }], // TODO: sound + [0x900, 0x9ff, 0, (a, v) => { this.banksel = v & 0x1; }], + [0xa00, 0xa07, 0x7, this.setBlitter.bind(this)], + [0xbff, 0xbff, 0, (a, v) => { if (v == 0x39) { this.watchdog_counter = INITIAL_WATCHDOG; this.watchdog_enabled = true; } }], + [0xc00, 0xfff, 0x3ff, (a, v) => { this.nvram[a] = v; }], + //[0x0, 0xfff, 0, function(a,v) { console.log('iowrite',hex(a),hex(v)); }], + ]); + + var memread_robotron = newAddressDecoder([ + [0x0000, 0x8fff, 0xffff, (a) => { return this.banksel ? this.rom[a] : this.ram[a]; }], + [0x9000, 0xbfff, 0xffff, (a) => { return this.ram[a]; }], + [0xc000, 0xcfff, 0x0fff, ioread_robotron], + [0xd000, 0xffff, 0xffff, (a) => { return this.rom ? this.rom[a - 0x4000] : 0; }], + ]); + + var memwrite_robotron = newAddressDecoder([ + [0x0000, 0x97ff, 0, this.write_display_byte.bind(this)], + [0x9800, 0xbfff, 0, (a, v) => { this.ram[a] = v; }], + [0xc000, 0xcfff, 0x0fff, iowrite_robotron.bind(this)], + //[0x0000, 0xffff, 0, function(a,v) { console.log(hex(a), hex(v)); }], + ]); + + var memread_williams = isDefender ? memread_defender : memread_robotron; + var memwrite_williams = isDefender ? memwrite_defender : memwrite_robotron; + this.membus = { + read: memread_williams, + write: memwrite_williams, + }; + this.membus = this.probeMemoryBus(this.membus); + this.readAddress = this.membus.read; + } + + initAudio() { + this.master = new MasterAudio(); + this.worker = new Worker("./src/common/audio/z80worker.js"); + let workerchannel = new WorkerSoundChannel(this.worker); + this.master.master.addChannel(workerchannel); + } + + initCPU() { + this.rom = new Uint8Array(this.defaultROMSize); + this.cpu = this.newCPU(this.membus); + //this.connectCPUMemoryBus(this); + } + + newCPU(membus: MemoryBus) { + var cpu = Object.create(CPU6809()); + cpu.init(membus.write, membus.read, 0); + return cpu; + } + + readAddress; + + // d1d6 ldu $11 / beq $d1ed + + setPalette(a, v) { + // RRRGGGBB + var color = 0xff000000 | ((v & 7) << 5) | (((v >> 3) & 7) << 13) | (((v >> 6) << 22)); + if (color != this.palette[a]) { + this.palette[a] = color; + this.screenNeedsRefresh = true; + } + } + + write_display_byte(a: number, v: number) { + this.ram[a] = v; + this.drawDisplayByte(a, v); + if (this.displayPCs) this.displayPCs[a] = this.cpu.getPC(); // save program counter + } + + drawDisplayByte(a, v) { + var ofs = ((a & 0xff00) << 1) | ((a & 0xff) ^ 0xff); + this.pixels[ofs] = this.palette[v >> 4]; + this.pixels[ofs + 256] = this.palette[v & 0xf]; + } + + setBlitter(a, v) { + if (a) { + this.blitregs[a] = v; + } else { + var cycles = this.doBlit(v); + this.waitCycles -= cycles * this.cpuScale; // wait CPU cycles + } + } + + doBlit(flags) { + //console.log(hex(flags), blitregs); + flags &= 0xff; + var offs = SCREEN_HEIGHT - this.blitregs[7]; + var sstart = (this.blitregs[2] << 8) + this.blitregs[3]; + var dstart = (this.blitregs[4] << 8) + this.blitregs[5]; + var w = this.blitregs[6] ^ 4; // blitter bug fix + var h = this.blitregs[7] ^ 4; + if (w == 0) w++; + if (h == 0) h++; + if (h == 255) h++; + var sxinc = (flags & 0x1) ? 256 : 1; + var syinc = (flags & 0x1) ? 1 : w; + var dxinc = (flags & 0x2) ? 256 : 1; + var dyinc = (flags & 0x2) ? 1 : w; + var pixdata = 0; + for (var y = 0; y < h; y++) { + var source = sstart & 0xffff; + var dest = dstart & 0xffff; + for (var x = 0; x < w; x++) { + var data = this.membus.read(source); + if (flags & 0x20) { + pixdata = (pixdata << 8) | data; + this.blit_pixel(dest, (pixdata >> 4) & 0xff, flags); + } else { + this.blit_pixel(dest, data, flags); + } + source += sxinc; + source &= 0xffff; + dest += dxinc; + dest &= 0xffff; + } + if (flags & 0x2) + dstart = (dstart & 0xff00) | ((dstart + dyinc) & 0xff); + else + dstart += dyinc; + if (flags & 0x1) + sstart = (sstart & 0xff00) | ((sstart + syinc) & 0xff); + else + sstart += syinc; + } + return w * h * (2 + ((flags & 0x4) >> 2)); // # of memory accesses + } + + blit_pixel(dstaddr, srcdata, flags) { + var curpix = dstaddr < 0xc000 ? this.ram[dstaddr] : this.membus.read(dstaddr); + var solid = this.blitregs[1]; + var keepmask = 0xff; //what part of original dst byte should be kept, based on NO_EVEN and NO_ODD flags + //even pixel (D7-D4) + if ((flags & 0x8) && !(srcdata & 0xf0)) { //FG only and src even pixel=0 + if (flags & 0x80) keepmask &= 0x0f; // no even + } else { + if (!(flags & 0x80)) keepmask &= 0x0f; // not no even + } + //odd pixel (D3-D0) + if ((flags & 0x8) && !(srcdata & 0x0f)) { //FG only and src odd pixel=0 + if (flags & 0x40) keepmask &= 0xf0; // no odd + } else { + if (!(flags & 0x40)) keepmask &= 0xf0; // not no odd + } + curpix &= keepmask; + if (flags & 0x10) // solid bit + curpix |= (solid & ~keepmask); + else + curpix |= (srcdata & ~keepmask); + if (dstaddr < 0x9800) // can cause recursion otherwise + this.membus.write(dstaddr, curpix); + } + + startScanline(): void { + this.audio && this.audioadapter && this.audioadapter.generate(this.audio); + // TODO: line-by-line + if (this.screenNeedsRefresh && this.scanline == 0) { + for (var i = 0; i < 0x9800; i++) + this.drawDisplayByte(i, this.ram[i]); + this.screenNeedsRefresh = false; + } + if (this.scanline == 0 && this.watchdog_enabled && this.watchdog_counter-- <= 0) { + console.log("WATCHDOG FIRED, PC =", this.cpu.getPC().toString(16)); // TODO: alert on video + // TODO: this.breakpointHit(cpu.T()); + this.reset(); + } + } + drawScanline(): void { + // interrupts happen every 1/4 of the screen + let sl = this.scanline; + if (sl == 0 || sl == 0x3c || sl == 0xbc || sl == 0xfc) { + if (!this.isDefender || this.pia6821[7] == 0x3c) { // TODO? + if (this.cpu.interrupt) + this.cpu.interrupt(); + if (this.cpu.requestInterrupt) + this.cpu.requestInterrupt(); + } + } + } + read(a: number): number { + return this.membus.read(a); + } + write(a: number, v: number): void { + this.membus.write(a, v); + } + readConst(a: number): number { + if (a >= 0xc000 && a <= 0xcbff) return 0xff; + else return this.membus.read(a); // TODO + } + reset() { + super.reset(); + this.watchdog_counter = INITIAL_WATCHDOG; + this.watchdog_enabled = false; + this.banksel = 1; + } + + loadSoundROM(data) { + console.log("loading sound ROM " + data.length + " bytes"); + var soundrom = padBytes(data, 0x4000); + this.worker.postMessage({ rom: soundrom }); + } + + loadROM(data) { + if (data.length > 2) { + if (this.isDefender) { + this.loadSoundROM(data.slice(0x6800)); + data = this.rom.slice(0, 0x6800); + } else if (data.length > 0xc000) { + this.loadSoundROM(data.slice(0xc000)); + data = this.rom.slice(0, 0xc000); + } else if (data.length > 0x9000 && data[0x9000]) { + this.loadSoundROM(data.slice(0x9000)); + } + data = padBytes(data, 0xc000); + } + super.loadROM(data); + } + + loadState(state) { + this.cpu.loadState(state.c); + this.ram.set(state.ram); + this.nvram.set(state.nvram); + this.pia6821.set(state.inputs); + this.blitregs.set(state.blt); + this.watchdog_counter = state.wdc; + this.banksel = state.bs; + this.portsel = state.ps; + } + saveState() { + return { + c: this.cpu.saveState(), + ram: this.ram.slice(0), + nvram: this.nvram.slice(0), + inputs: this.pia6821.slice(0), + blt: this.blitregs.slice(0), + wdc: this.watchdog_counter, + bs: this.banksel, + ps: this.portsel, + }; + } + loadControlsState(state) { + this.pia6821.set(state.inputs); + } + saveControlsState() { + return { + inputs: this.pia6821.slice(0), + }; + } +} diff --git a/src/platform/williams.ts b/src/platform/williams.ts index a760d6e4..0d478eef 100644 --- a/src/platform/williams.ts +++ b/src/platform/williams.ts @@ -1,8 +1,11 @@ -import { Platform, BaseZ80Platform, Base6809Platform } from "../common/baseplatform"; +import { Platform, BaseZ80Platform, Base6809Platform, Base6809MachinePlatform } from "../common/baseplatform"; import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap } from "../common/emu"; import { hex } from "../common/util"; import { MasterAudio, WorkerSoundChannel } from "../common/audio"; +import { WilliamsMachine } from "../machine/williams"; + +// https://www.arcade-museum.com/manuals-videogames/D/Defender.pdf var WILLIAMS_PRESETS = [ { id: 'gfxtest.c', name: 'Graphics Test' }, @@ -469,7 +472,23 @@ var WilliamsDefenderPlatform = function(mainElement, options) { ] } }; } -PLATFORMS['williams'] = Williams6809Platform; +class NewWilliamsPlatform extends Base6809MachinePlatform implements Platform { + + newMachine() { return new WilliamsMachine(false); } + getPresets() { return WILLIAMS_PRESETS; } + getDefaultExtension() { return ".c"; }; + readAddress(a) { return this.machine.readConst(a); } + getMemoryMap() { + return { main:[ + {name:'Video RAM',start:0x0000,size:0xc000,type:'ram'}, + {name:'I/O Registers',start:0xc000,size:0xc00,type:'io'}, + {name:'NVRAM',start:0xcc00,size:0x400,type:'ram'}, + {name:'ROM',start:0xd000,size:0x3000,type:'rom'}, + ] } }; +} + +PLATFORMS['williams'] = NewWilliamsPlatform; +PLATFORMS['williams.old'] = Williams6809Platform; PLATFORMS['williams-defender'] = WilliamsDefenderPlatform; PLATFORMS['williams-z80'] = WilliamsZ80Platform;