diff --git a/src/common/audio.ts b/src/common/audio.ts index e637b95a..700f0e45 100644 --- a/src/common/audio.ts +++ b/src/common/audio.ts @@ -511,21 +511,38 @@ export class SampledAudio { import { SampledAudioSink } from "./devices"; +interface TssChannel { + setBufferLength(len : number) : void; + setSampleRate(rate : number) : void; + getBuffer() : number[]; + generate(numSamples : number) : void; +} + export class TssChannelAdapter { - channel; + channels : TssChannel[]; audioGain = 1.0 / 8192; - constructor(channel, oversample:number, sampleRate:number) { - this.channel = channel; - channel.setBufferLength(oversample*2); - channel.setSampleRate(sampleRate); + bufferLength : number; + + constructor(chans, oversample:number, sampleRate:number) { + this.bufferLength = oversample * 2; + this.channels = chans.generate ? [chans] : chans; // array or single channel + this.channels.forEach((c) => { + c.setBufferLength(this.bufferLength); + c.setSampleRate(sampleRate); + }); } + generate(sink:SampledAudioSink) { - var buf = this.channel.getBuffer(); - var l = buf.length; - this.channel.generate(l); - for (let i=0; i ch.getBuffer()); + this.channels.forEach((ch) => { + ch.generate(l); + }); + for (let i=0; i total += buf[i]); + sink.feedSample(total * this.audioGain, 1); + }; } } diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index 5326bb84..3bae22df 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -33,6 +33,7 @@ interface AppleIIState extends AppleIIStateBase, AppleIIControlsState { interface SlotDevice extends Bus { readROM(address: number) : number; + readConst(address: number) : number; } export class AppleII extends BasicScanlineMachine { @@ -81,6 +82,9 @@ export class AppleII extends BasicScanlineMachine { default: return 0; } }, + readConst: (a) => { + return 0; + }, read: (a) => { return this.noise(); }, write: (a,v) => { } }; @@ -176,7 +180,7 @@ export class AppleII extends BasicScanlineMachine { return this.ram[address + this.bank2rdoffset]; } else if (address >= 0xc100 && address < 0xc800) { var slot = (address >> 8) & 7; - return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0; + return (this.slots[slot] && this.slots[slot].readConst(address & 0xff)) | 0; } else { return 0; } @@ -231,8 +235,9 @@ export class AppleII extends BasicScanlineMachine { return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0; } } else if (address >= 0xc100 && address < 0xc800) { - return this.readConst(address); - } + var slot = (address >> 8) & 7; + return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0; + } return this.noise(); } write(address:number, val:number) : void { @@ -1091,6 +1096,7 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState } readROM(address) { return DISKII_PROM[address]; } + readConst(address) { return DISKII_PROM[address]; } read(address) { return this.doIO(address, 0); } write(address, value) { this.doIO(address, value); } diff --git a/src/machine/galaxian.ts b/src/machine/galaxian.ts new file mode 100644 index 00000000..c6700702 --- /dev/null +++ b/src/machine/galaxian.ts @@ -0,0 +1,406 @@ + +import { Z80, Z80State } from "../common/cpu/ZilogZ80"; +import { BasicScanlineMachine } from "../common/devices"; +import { KeyFlags, newAddressDecoder, padBytes, noise, Keys, makeKeycodeMap, newKeyboardHandler, EmuHalt } from "../common/emu"; +import { TssChannelAdapter, MasterAudio, AY38910_Audio } from "../common/audio"; +import { hex } from "../common/util"; + +const GALAXIAN_KEYCODE_MAP = makeKeycodeMap([ + [Keys.A, 0, 0x10], // P1 + [Keys.LEFT, 0, 0x4], + [Keys.RIGHT, 0, 0x8], + [Keys.P2_A, 1, 0x10], // P2 + [Keys.P2_LEFT, 1, 0x4], + [Keys.P2_RIGHT, 1, 0x8], + [Keys.SELECT, 0, 0x1], + [Keys.START, 1, 0x1], + [Keys.VK_2, 1, 0x2], +]); + +const SCRAMBLE_KEYCODE_MAP = makeKeycodeMap([ + [Keys.UP, 0, -0x1], // P1 + [Keys.B, 0, -0x2], // fire + [Keys.VK_7, 0, -0x4], // credit + [Keys.A, 0, -0x8], // bomb + [Keys.RIGHT, 0, -0x10], + [Keys.LEFT, 0, -0x20], + [Keys.VK_6, 0, -0x40], + [Keys.SELECT, 0, -0x80], + [Keys.START, 1, -0x80], + [Keys.VK_2, 1, -0x40], + [Keys.DOWN, 2, -0x40], + //[Keys.VK_UP, 2, -0x10], +]); + +const bitcolors = [ + 0x000021, 0x000047, 0x000097, // red + 0x002100, 0x004700, 0x009700, // green + 0x510000, 0xae0000 // blue +]; + +const GalaxianVideo = function (rom: Uint8Array, vram: Uint8Array, oram: Uint8Array, palette: Uint32Array, options) { + + var gfxBase = options.gfxBase || 0x2800; + this.missileWidth = options.missileWidth || 4; + this.missileOffset = options.missileOffset || 0; + this.showOffscreenObjects = false; + this.frameCounter = 0; + this.starsEnabled = 0; + var stars = []; + for (var i = 0; i < 256; i++) + stars[i] = noise(); + + this.advanceFrame = function () { + this.frameCounter = (this.frameCounter + 1) & 0xff; + } + + this.drawScanline = function (pixels, sl) { + var pixofs = sl * 264; + // hide offscreen on left + right (b/c rotated) + if (!this.showOffscreenObjects && (sl < 16 || sl >= 240)) { + for (var i = 0; i < 264; i++) + pixels[pixofs + i] = 0xff000000; + return; // offscreen + } + // draw tiles + var outi = pixofs; // starting output pixel in frame buffer + for (var xx = 0; xx < 32; xx++) { + var xofs = xx; + var scroll = oram[xofs * 2]; // even entries control scroll position + var attrib = oram[xofs * 2 + 1]; // odd entries control the color base + var sl2 = (sl + scroll) & 0xff; + var vramofs = (sl2 >> 3) << 5; // offset in VRAM + var yy = sl2 & 7; // y offset within tile + var tile = vram[vramofs + xofs]; // TODO: why undefined? + var color0 = (attrib & 7) << 2; + var addr = gfxBase + (tile << 3) + yy; + var data1 = rom[addr]; + var data2 = rom[addr + 0x800]; + for (var i = 0; i < 8; i++) { + var bm = 128 >> i; + var color = color0 + ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); + pixels[outi] = palette[color]; + outi++; + } + } + // draw sprites + for (var sprnum = 7; sprnum >= 0; sprnum--) { + var base = (sprnum << 2) + 0x40; + var base0 = oram[base]; + var sy = 240 - (base0 - ((sprnum < 3) ? 1 : 0)); // the first three sprites match against y-1 + var yy = (sl - sy); + if (yy >= 0 && yy < 16) { + var sx = oram[base + 3] + 1; // +1 pixel offset from tiles + if (sx == 0 && !this.showOffscreenObjects) { + continue; // drawn off-buffer + } + var code = oram[base + 1]; + var flipx = code & 0x40; // TODO: flipx + if (code & 0x80) // flipy + yy = 15 - yy; + code &= 0x3f; + var color0 = (oram[base + 2] & 7) << 2; + var addr = gfxBase + (code << 5) + (yy < 8 ? yy : yy + 8); + outi = pixofs + sx; //<< 1 + var data1 = rom[addr]; + var data2 = rom[addr + 0x800]; + for (var i = 0; i < 8; i++) { + var bm = 128 >> i; + var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); + if (color) + pixels[flipx ? (outi + 15 - i) : (outi + i)] = palette[color0 + color]; + } + var data1 = rom[addr + 8]; + var data2 = rom[addr + 0x808]; + for (var i = 0; i < 8; i++) { + var bm = 128 >> i; + var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); + if (color) + pixels[flipx ? (outi + 7 - i) : (outi + i + 8)] = palette[color0 + color]; + } + } + } + // draw bullets/shells + var shell = 0xff; + var missile = 0xff; + for (var which = 0; which < 8; which++) { + var sy = oram[0x60 + (which << 2) + 1]; + if (((sy + sl - ((which < 3) ? 1 : 0)) & 0xff) == 0xff) { + if (which != 7) + shell = which; + else + missile = which; + } + } + for (var i = 0; i < 2; i++) { + which = i ? missile : shell; + if (which != 0xff) { + var sx = 255 - oram[0x60 + (which << 2) + 3]; + var outi = pixofs + sx - this.missileOffset; + var col = which == 7 ? 0xffffff00 : 0xffffffff; + for (var j = 0; j < this.missileWidth; j++) + pixels[outi++] = col; + } + } + // draw stars + if (this.starsEnabled) { + var starx = ((this.frameCounter + stars[sl & 0xff]) & 0xff); + if ((starx + sl) & 0x10) { + var outi = pixofs + starx; + if ((pixels[outi] & 0xffffff) == 0) { + pixels[outi] = palette[sl & 0x1f]; + } + } + } + } + +} + +const XTAL = 18432000.0; +const scanlinesPerFrame = 264; +const cpuFrequency = XTAL / 6; // 3.072 MHz +const hsyncFrequency = XTAL / 3 / 192 / 2; // 16 kHz +const vsyncFrequency = hsyncFrequency / 132 / 2; // 60.606060 Hz +const vblankDuration = 1 / vsyncFrequency * (20 / 132); // 2500 us +const cpuCyclesPerLine = cpuFrequency / hsyncFrequency; +const INITIAL_WATCHDOG = 8; + +const audioOversample = 2; +const audioSampleRate = 60 * scanlinesPerFrame; // why not hsync? + +export class GalaxianMachine extends BasicScanlineMachine { + + options = {}; + palBase = 0x3800; + keyMap = GALAXIAN_KEYCODE_MAP; + + cpuFrequency = cpuFrequency; + canvasWidth = 264; + numTotalScanlines = 264; + numVisibleScanlines = 264; + defaultROMSize = 0x4000; + sampleRate = audioSampleRate * audioOversample; + cpuCyclesPerLine = cpuCyclesPerLine | 0; + rotate = 90; + + cpu: Z80 = new Z80(); + ram = new Uint8Array(0x800); + vram = new Uint8Array(0x400); + oram = new Uint8Array(0x100); + palette: Uint32Array; + gfx; // GalaxianVideo + audioadapter; + psg1: AY38910_Audio; + psg2: AY38910_Audio; + watchdog_counter: number = 0; + interruptEnabled: number = 0; + defaultInputs: number[] = [0xe, 0x8, 0x0]; + + constructor() { + super(); + var audio = new MasterAudio(); + this.psg1 = new AY38910_Audio(audio); + this.psg2 = new AY38910_Audio(audio); + this.audioadapter = new TssChannelAdapter([this.psg1.psg, this.psg2.psg], audioOversample, this.sampleRate); + this.init(); + } + + init() { + this.rom = new Uint8Array(this.defaultROMSize); + this.palette = new Uint32Array(new ArrayBuffer(32 * 4)); + this.gfx = new GalaxianVideo(this.rom, this.vram, this.oram, this.palette, this.options); + this.connectCPUMemoryBus(this); + this.connectCPUIOBus(this.newIOBus()); + this.inputs.set(this.defaultInputs); + this.handler = newKeyboardHandler(this.inputs, this.keyMap); + } + + read = newAddressDecoder([ + [0x0000, 0x3fff, 0, (a) => { return this.rom ? this.rom[a] : null; }], + [0x4000, 0x47ff, 0x3ff, (a) => { return this.ram[a]; }], + [0x5000, 0x57ff, 0x3ff, (a) => { return this.vram[a]; }], + [0x5800, 0x5fff, 0xff, (a) => { return this.oram[a]; }], + [0x6000, 0x6000, 0, (a) => { return this.inputs[0]; }], + [0x6800, 0x6800, 0, (a) => { return this.inputs[1]; }], + [0x7000, 0x7000, 0, (a) => { return this.inputs[2]; }], + [0x7800, 0x7800, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }], + ]); + + readConst(a : number) { + return (a < 0x7000) ? this.read(a) : null; + } + + write = newAddressDecoder([ + [0x4000, 0x47ff, 0x3ff, (a, v) => { this.ram[a] = v; }], + [0x5000, 0x57ff, 0x3ff, (a, v) => { this.vram[a] = v; }], + [0x5800, 0x5fff, 0xff, (a, v) => { this.oram[a] = v; }], + //[0x6004, 0x6007, 0x3, function(a,v) => { }], // lfo freq + //[0x6800, 0x6807, 0x7, function(a,v) => { }], // sound + //[0x7800, 0x7800, 0x7, function(a,v) => { }], // pitch + //[0x6000, 0x6003, 0x3, (a, v) => { this.outlatches[a] = v; }], + [0x7001, 0x7001, 0, (a, v) => { this.interruptEnabled = v & 1; }], + [0x7004, 0x7004, 0, (a, v) => { this.gfx.starsEnabled = v & 1; }], + ]); + + newIOBus() { + return { + read: (addr) => { + return 0; + }, + write: (addr, val) => { + if (addr & 0x1) { this.psg1.selectRegister(val & 0xf); }; + if (addr & 0x2) { this.psg1.setData(val); }; + if (addr & 0x4) { this.psg2.selectRegister(val & 0xf); }; + if (addr & 0x8) { this.psg2.setData(val); }; + } + }; + } + + reset() { + super.reset(); + this.psg1.reset(); + this.psg2.reset(); + this.watchdog_counter = INITIAL_WATCHDOG; + } + + startScanline() { + this.audio && this.audioadapter && this.audioadapter.generate(this.audio); + } + + drawScanline() { + this.gfx.drawScanline(this.pixels, this.scanline); + } + + advanceFrame(trap) { + var steps = super.advanceFrame(trap); + + // advance graphics + this.gfx.advanceFrame(); + // clear bottom of screen? + if (!this.gfx.showOffscreenObjects) { + for (var i = 0; i < 264; i++) + this.pixels.fill(0xff000000, 256 + i * 264, 264 + i * 264); + } + // watchdog fired? + if (this.watchdog_counter-- <= 0) { + throw new EmuHalt("WATCHDOG FIRED"); + } + // NMI interrupt @ 0x66 + if (this.interruptEnabled) { this.cpu.NMI(); } + + return steps; + } + + loadROM(data) { + this.rom.set(padBytes(data, this.defaultROMSize)); + for (var i = 0; i < 32; i++) { + var b = this.rom[this.palBase + i]; + this.palette[i] = 0xff000000; + for (var j = 0; j < 8; j++) + if (((1 << j) & b)) + this.palette[i] += bitcolors[j]; + } + } + + loadState(state) { + super.loadState(state); + this.vram.set(state.bv); + this.oram.set(state.bo); + this.watchdog_counter = state.wdc; + this.interruptEnabled = state.ie; + this.gfx.starsEnabled = state.se; + this.gfx.frameCounter = state.fc; + } + + saveState() { + var state = super.saveState(); + state['bv'] = this.vram.slice(0); + state['bo'] = this.oram.slice(0); + state['fc'] = this.gfx.frameCounter; + state['ie'] = this.interruptEnabled; + state['se'] = this.gfx.starsEnabled; + state['wdc'] = this.watchdog_counter; + return state; + } + +} + +export class GalaxianScrambleMachine extends GalaxianMachine { + + defaultROMSize = 0x5020; + palBase = 0x5000; + scramble = true; + keyMap = SCRAMBLE_KEYCODE_MAP; + options = { + gfxBase: 0x4000, + missileWidth: 1, + missileOffset: 6, + }; + defaultInputs = [0xff, 0xfc, 0xf1]; + + constructor() { + super(); + this.init(); // TODO: why do we have to call twice? + } + + read = newAddressDecoder([ + [0x0000, 0x3fff, 0, (a) => { return this.rom[a]; }], + [0x4000, 0x47ff, 0x7ff, (a) => { return this.ram[a]; }], + [0x4800, 0x4fff, 0x3ff, (a) => { return this.vram[a]; }], + [0x5000, 0x5fff, 0xff, (a) => { return this.oram[a]; }], + [0x7000, 0x7000, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }], + [0x7800, 0x7800, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }], + //[0x8000, 0x820f, 0, function(a) { return noise(); }], // TODO: remove + [0x8100, 0x8100, 0, (a) => { return this.inputs[0]; }], + [0x8101, 0x8101, 0, (a) => { return this.inputs[1]; }], + [0x8102, 0x8102, 0, (a) => { return this.inputs[2] | this.scramble_protection_alt_r(); }], + [0x8202, 0x8202, 0, (a) => { return this.m_protection_result; }], // scramble (protection) + [0x9100, 0x9100, 0, (a) => { return this.inputs[0]; }], + [0x9101, 0x9101, 0, (a) => { return this.inputs[1]; }], + [0x9102, 0x9102, 0, (a) => { return this.inputs[2] | this.scramble_protection_alt_r(); }], + [0x9212, 0x9212, 0, (a) => { return this.m_protection_result; }], // scramble (protection) + //[0, 0xffff, 0, function(a) { console.log(hex(a)); return 0; }] + ]); + write = newAddressDecoder([ + [0x4000, 0x47ff, 0x7ff, (a, v) => { this.ram[a] = v; }], + [0x4800, 0x4fff, 0x3ff, (a, v) => { this.vram[a] = v; }], + [0x5000, 0x5fff, 0xff, (a, v) => { this.oram[a] = v; }], + [0x6801, 0x6801, 0, (a, v) => { this.interruptEnabled = v & 1; /*console.log(a,v,cpu.getPC().toString(16));*/ }], + [0x6802, 0x6802, 0, (a, v) => { /* TODO: coin counter */ }], + [0x6803, 0x6803, 0, (a, v) => { /* TODO: backgroundColor = (v & 1) ? 0xFF000056 : 0xFF000000; */ }], + [0x6804, 0x6804, 0, (a, v) => { this.gfx.starsEnabled = v & 1; }], + [0x6808, 0x6808, 0, (a, v) => { this.gfx.missileWidth = v; }], // not on h/w + [0x6809, 0x6809, 0, (a, v) => { this.gfx.missileOffset = v; }], // not on h/w + [0x8202, 0x8202, 0, this.scramble_protection_w.bind(this)], + //[0x8100, 0x8103, 0, function(a,v){ /* PPI 0 */ }], + //[0x8200, 0x8203, 0, function(a,v){ /* PPI 1 */ }], + //[0, 0xffff, 0, function(a,v) { console.log(hex(a),hex(v)); }] + ]); + + m_protection_state = 0; + m_protection_result = 0; + scramble_protection_w(addr, data) { + /* + This is not fully understood; the low 4 bits of port C are + inputs; the upper 4 bits are outputs. Scramble main set always + writes sequences of 3 or more nibbles to the low port and + expects certain results in the upper nibble afterwards. + */ + this.m_protection_state = (this.m_protection_state << 4) | (data & 0x0f); + switch (this.m_protection_state & 0xfff) { + /* scramble */ + case 0xf09: this.m_protection_result = 0xff; break; + case 0xa49: this.m_protection_result = 0xbf; break; + case 0x319: this.m_protection_result = 0x4f; break; + case 0x5c9: this.m_protection_result = 0x6f; break; + + /* scrambls */ + case 0x246: this.m_protection_result ^= 0x80; break; + case 0xb5f: this.m_protection_result = 0x6f; break; + } + } + scramble_protection_alt_r() { + var bit = (this.m_protection_result >> 7) & 1; + return (bit << 5) | ((bit ^ 1) << 7); + } +} diff --git a/src/platform/galaxian.ts b/src/platform/galaxian.ts index 556c9bef..8fadba6b 100644 --- a/src/platform/galaxian.ts +++ b/src/platform/galaxian.ts @@ -1,442 +1,34 @@ -import { Platform, BaseZ80Platform } from "../common/baseplatform"; -import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap } from "../common/emu"; -import { hex } from "../common/util"; -import { MasterAudio, AY38910_Audio } from "../common/audio"; +import { Platform } from "../common/baseplatform"; +import { PLATFORMS } from "../common/emu"; +import { GalaxianMachine, GalaxianScrambleMachine } from "../machine/galaxian"; +import { BaseZ80MachinePlatform } from "../common/baseplatform"; const GALAXIAN_PRESETS = [ { id: 'gfxtest.c', name: 'Graphics Test' }, { id: 'shoot2.c', name: 'Solarian Game' }, ]; -const GALAXIAN_KEYCODE_MAP = makeKeycodeMap([ - [Keys.A, 0, 0x10], // P1 - [Keys.LEFT, 0, 0x4], - [Keys.RIGHT, 0, 0x8], - [Keys.P2_A, 1, 0x10], // P2 - [Keys.P2_LEFT, 1, 0x4], - [Keys.P2_RIGHT, 1, 0x8], - [Keys.SELECT, 0, 0x1], - [Keys.START, 1, 0x1], - [Keys.VK_2, 1, 0x2], -]); +class GalaxianPlatform extends BaseZ80MachinePlatform implements Platform { -const SCRAMBLE_KEYCODE_MAP = makeKeycodeMap([ - [Keys.UP, 0, -0x1], // P1 - [Keys.B, 0, -0x2], // fire - [Keys.VK_7, 0, -0x4], // credit - [Keys.A, 0, -0x8], // bomb - [Keys.RIGHT, 0, -0x10], - [Keys.LEFT, 0, -0x20], - [Keys.VK_6, 0, -0x40], - [Keys.SELECT, 0, -0x80], - [Keys.START, 1, -0x80], - [Keys.VK_2, 1, -0x40], - [Keys.DOWN, 2, -0x40], - //[Keys.VK_UP, 2, -0x10], -]); + newMachine() { return new GalaxianMachine(); } + getPresets() { return GALAXIAN_PRESETS; } + getDefaultExtension() { return ".c"; }; + readAddress(a) { return this.machine.readConst(a); } + readVRAMAddress(a) { return (a < 0x800) ? this.machine.vram[a] : this.machine.oram[a-0x800]; } + // TODO loadBIOS(bios) { this.machine.loadBIOS(a); } + getMemoryMap = function() { return { main:[ + {name:'Video RAM',start:0x5000,size:0x400,type:'ram'}, + {name:'Sprite RAM',start:0x5800,size:0x100,type:'ram'}, + {name:'I/O Registers',start:0x6000,size:0x2000,type:'io'}, + ] } }; +} -const GalaxianVideo = function(rom:Uint8Array, vram:RAM, oram:RAM, palette:Uint32Array, options) { +class GalaxianScramblePlatform extends GalaxianPlatform implements Platform { - var gfxBase = options.gfxBase || 0x2800; - this.missileWidth = options.missileWidth || 4; - this.missileOffset = options.missileOffset || 0; - this.showOffscreenObjects = false; - this.frameCounter = 0; - this.starsEnabled = 0; - var stars = []; - for (var i = 0; i < 256; i++) - stars[i] = noise(); - - this.advanceFrame = function() { - this.frameCounter = (this.frameCounter + 1) & 0xff; - } - - this.drawScanline = function(pixels, sl) { - if (sl < 16 && !this.showOffscreenObjects) return; // offscreen - if (sl >= 240 && !this.showOffscreenObjects) return; // offscreen - // draw tiles - var pixofs = sl * 264; - var outi = pixofs; // starting output pixel in frame buffer - for (var xx = 0; xx < 32; xx++) { - var xofs = xx; - var scroll = oram.mem[xofs * 2]; // even entries control scroll position - var attrib = oram.mem[xofs * 2 + 1]; // odd entries control the color base - var sl2 = (sl + scroll) & 0xff; - var vramofs = (sl2 >> 3) << 5; // offset in VRAM - var yy = sl2 & 7; // y offset within tile - var tile = vram.mem[vramofs + xofs]; // TODO: why undefined? - var color0 = (attrib & 7) << 2; - var addr = gfxBase + (tile << 3) + yy; - var data1 = rom[addr]; - var data2 = rom[addr + 0x800]; - for (var i = 0; i < 8; i++) { - var bm = 128 >> i; - var color = color0 + ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); - pixels[outi] = palette[color]; - outi++; - } - } - // draw sprites - for (var sprnum = 7; sprnum >= 0; sprnum--) { - var base = (sprnum << 2) + 0x40; - var base0 = oram.mem[base]; - var sy = 240 - (base0 - ((sprnum < 3) ? 1 : 0)); // the first three sprites match against y-1 - var yy = (sl - sy); - if (yy >= 0 && yy < 16) { - var sx = oram.mem[base + 3] + 1; // +1 pixel offset from tiles - if (sx == 0 && !this.showOffscreenObjects) - continue; // drawn off-buffer - var code = oram.mem[base + 1]; - var flipx = code & 0x40; // TODO: flipx - if (code & 0x80) // flipy - yy = 15 - yy; - code &= 0x3f; - var color0 = (oram.mem[base + 2] & 7) << 2; - var addr = gfxBase + (code << 5) + (yy < 8 ? yy : yy + 8); - outi = pixofs + sx; //<< 1 - var data1 = rom[addr]; - var data2 = rom[addr + 0x800]; - for (var i = 0; i < 8; i++) { - var bm = 128 >> i; - var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); - if (color) - pixels[flipx ? (outi + 15 - i) : (outi + i)] = palette[color0 + color]; - } - var data1 = rom[addr + 8]; - var data2 = rom[addr + 0x808]; - for (var i = 0; i < 8; i++) { - var bm = 128 >> i; - var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); - if (color) - pixels[flipx ? (outi + 7 - i) : (outi + i + 8)] = palette[color0 + color]; - } - } - } - // draw bullets/shells - var shell = 0xff; - var missile = 0xff; - for (var which = 0; which < 8; which++) { - var sy = oram.mem[0x60 + (which << 2) + 1]; - if (((sy + sl - ((which < 3) ? 1 : 0)) & 0xff) == 0xff) { - if (which != 7) - shell = which; - else - missile = which; - } - } - for (var i = 0; i < 2; i++) { - which = i ? missile : shell; - if (which != 0xff) { - var sx = 255 - oram.mem[0x60 + (which << 2) + 3]; - var outi = pixofs + sx - this.missileOffset; - var col = which == 7 ? 0xffffff00 : 0xffffffff; - for (var j = 0; j < this.missileWidth; j++) - pixels[outi++] = col; - } - } - // draw stars - if (this.starsEnabled) { - var starx = ((this.frameCounter + stars[sl & 0xff]) & 0xff); - if ((starx + sl) & 0x10) { - var outi = pixofs + starx; - if ((pixels[outi] & 0xffffff) == 0) { - pixels[outi] = palette[sl & 0x1f]; - } - } - } - } + newMachine() { return new GalaxianScrambleMachine(); } } -const _GalaxianPlatform = function(mainElement, options) { - options = options || {}; - var romSize = options.romSize || 0x4000; - var palBase = options.palBase || 0x3800; - var keyMap = options.keyMap || GALAXIAN_KEYCODE_MAP; - - var cpu; - var ram, vram, oram: RAM; - var membus, iobus, rom, palette, outlatches; - var video, audio, timer, pixels; - var psg1, psg2; - var inputs; - var interruptEnabled = 0; - var watchdog_counter; - - var XTAL = 18432000.0; - var scanlinesPerFrame = 264; - var cpuFrequency = XTAL / 6; // 3.072 MHz - var hsyncFrequency = XTAL / 3 / 192 / 2; // 16 kHz - var vsyncFrequency = hsyncFrequency / 132 / 2; // 60.606060 Hz - var vblankDuration = 1 / vsyncFrequency * (20 / 132); // 2500 us - var cpuCyclesPerLine = cpuFrequency / hsyncFrequency; - var INITIAL_WATCHDOG = 8; - var gfx; // = new GalaxianVideo(rom, vram, oram, palette, options); - - var m_protection_state = 0; - var m_protection_result = 0; - function scramble_protection_w(addr, data) { - /* - This is not fully understood; the low 4 bits of port C are - inputs; the upper 4 bits are outputs. Scramble main set always - writes sequences of 3 or more nibbles to the low port and - expects certain results in the upper nibble afterwards. - */ - m_protection_state = (m_protection_state << 4) | (data & 0x0f); - switch (m_protection_state & 0xfff) { - /* scramble */ - case 0xf09: m_protection_result = 0xff; break; - case 0xa49: m_protection_result = 0xbf; break; - case 0x319: m_protection_result = 0x4f; break; - case 0x5c9: m_protection_result = 0x6f; break; - - /* scrambls */ - case 0x246: m_protection_result ^= 0x80; break; - case 0xb5f: m_protection_result = 0x6f; break; - } - } - function scramble_protection_alt_r() { - var bit = (m_protection_result >> 7) & 1; - return (bit << 5) | ((bit ^ 1) << 7); - } - - const bitcolors = [ - 0x000021, 0x000047, 0x000097, // red - 0x002100, 0x004700, 0x009700, // green - 0x510000, 0xae0000 // blue - ]; - - class GalaxianPlatform extends BaseZ80Platform implements Platform { - - scanline: number; - poller; - - getPresets() { - return GALAXIAN_PRESETS; - } - - start() { - ram = new RAM(0x800); - vram = new RAM(0x400); - oram = new RAM(0x100); - rom = new Uint8Array(romSize); - palette = new Uint32Array(new ArrayBuffer(32 * 4)); - gfx = new GalaxianVideo(rom, vram, oram, palette, options); - - outlatches = new RAM(0x8); - if (options.scramble) { - inputs = [0xff, 0xfc, 0xf1]; - membus = { - read: newAddressDecoder([ - [0x0000, 0x3fff, 0, function(a) { return rom ? rom[a] : null; }], - [0x4000, 0x47ff, 0x7ff, function(a) { return ram.mem[a]; }], - [0x4800, 0x4fff, 0x3ff, function(a) { return vram.mem[a]; }], - [0x5000, 0x5fff, 0xff, function(a) { return oram.mem[a]; }], - [0x7000, 0x7000, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], - [0x7800, 0x7800, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], - //[0x8000, 0x820f, 0, function(a) { return noise(); }], // TODO: remove - [0x8100, 0x8100, 0, function(a) { return inputs[0]; }], - [0x8101, 0x8101, 0, function(a) { return inputs[1]; }], - [0x8102, 0x8102, 0, function(a) { return inputs[2] | scramble_protection_alt_r(); }], - [0x8202, 0x8202, 0, function(a) { return m_protection_result; }], // scramble (protection) - [0x9100, 0x9100, 0, function(a) { return inputs[0]; }], - [0x9101, 0x9101, 0, function(a) { return inputs[1]; }], - [0x9102, 0x9102, 0, function(a) { return inputs[2] | scramble_protection_alt_r(); }], - [0x9212, 0x9212, 0, function(a) { return m_protection_result; }], // scramble (protection) - //[0, 0xffff, 0, function(a) { console.log(hex(a)); return 0; }] - ]), - write: newAddressDecoder([ - [0x4000, 0x47ff, 0x7ff, function(a, v) { ram.mem[a] = v; }], - [0x4800, 0x4fff, 0x3ff, function(a, v) { vram.mem[a] = v; }], - [0x5000, 0x5fff, 0xff, function(a, v) { oram.mem[a] = v; }], - [0x6801, 0x6801, 0, function(a, v) { interruptEnabled = v & 1; /*console.log(a,v,cpu.getPC().toString(16));*/ }], - [0x6802, 0x6802, 0, function(a, v) { /* TODO: coin counter */ }], - [0x6803, 0x6803, 0, function(a, v) { /* TODO: backgroundColor = (v & 1) ? 0xFF000056 : 0xFF000000; */ }], - [0x6804, 0x6804, 0, function(a, v) { gfx.starsEnabled = v & 1; }], - [0x6808, 0x6808, 0, function(a, v) { gfx.missileWidth = v; }], // not on h/w - [0x6809, 0x6809, 0, function(a, v) { gfx.missileOffset = v; }], // not on h/w - [0x8202, 0x8202, 0, scramble_protection_w], - //[0x8100, 0x8103, 0, function(a,v){ /* PPI 0 */ }], - //[0x8200, 0x8203, 0, function(a,v){ /* PPI 1 */ }], - //[0, 0xffff, 0, function(a,v) { console.log(hex(a),hex(v)); }] - ]), - }; - } else { - inputs = [0xe, 0x8, 0x0]; - membus = { - read: newAddressDecoder([ - [0x0000, 0x3fff, 0, function(a) { return rom ? rom[a] : null; }], - [0x4000, 0x47ff, 0x3ff, function(a) { return ram.mem[a]; }], - [0x5000, 0x57ff, 0x3ff, function(a) { return vram.mem[a]; }], - [0x5800, 0x5fff, 0xff, function(a) { return oram.mem[a]; }], - [0x6000, 0x6000, 0, function(a) { return inputs[0]; }], - [0x6800, 0x6800, 0, function(a) { return inputs[1]; }], - [0x7000, 0x7000, 0, function(a) { return inputs[2]; }], - [0x7800, 0x7800, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], - ]), - write: newAddressDecoder([ - [0x4000, 0x47ff, 0x3ff, function(a, v) { ram.mem[a] = v; }], - [0x5000, 0x57ff, 0x3ff, function(a, v) { vram.mem[a] = v; }], - [0x5800, 0x5fff, 0xff, function(a, v) { oram.mem[a] = v; }], - //[0x6004, 0x6007, 0x3, function(a,v) { }], // lfo freq - //[0x6800, 0x6807, 0x7, function(a,v) { }], // sound - //[0x7800, 0x7800, 0x7, function(a,v) { }], // pitch - [0x6000, 0x6003, 0x3, function(a, v) { outlatches.mem[a] = v; }], - [0x7001, 0x7001, 0, function(a, v) { interruptEnabled = v & 1; }], - [0x7004, 0x7004, 0, function(a, v) { gfx.starsEnabled = v & 1; }], - ]), - isContended: function() { return false; }, - }; - } - audio = new MasterAudio(); - psg1 = new AY38910_Audio(audio); - psg2 = new AY38910_Audio(audio); - iobus = { - read: function(addr) { - return 0; - }, - write: function(addr, val) { - if (addr & 0x1) { psg1.selectRegister(val & 0xf); }; - if (addr & 0x2) { psg1.setData(val); }; - if (addr & 0x4) { psg2.selectRegister(val & 0xf); }; - if (addr & 0x8) { psg2.setData(val); }; - } - }; - cpu = this.newCPU(membus, iobus); - video = new RasterVideo(mainElement, 264, 264, { rotate: 90 }); - video.create(); - var idata = video.getFrameData(); - this.poller = setKeyboardFromMap(video, inputs, keyMap); - pixels = video.getFrameData(); - timer = new AnimationTimer(60, this.nextFrame.bind(this)); - } - - pollControls() { this.poller.poll(); } - - readAddress(a) { - return (a == 0x7000 || a == 0x7800) ? null : membus.read(a); // ignore watchdog - } - - advance(novideo: boolean) : number { - var steps = 0; - for (var sl = 0; sl < scanlinesPerFrame; sl++) { - this.scanline = sl; - if (!novideo) { - gfx.drawScanline(pixels, sl); - } - steps += this.runCPU(cpu, cpuCyclesPerLine); - } - // visible area is 256x224 (before rotation) - if (!novideo) { - video.updateFrame(0, 0, 0, 0, gfx.showOffscreenObjects ? 264 : 256, 264); - } - gfx.advanceFrame(); - if (watchdog_counter-- <= 0) { - console.log("WATCHDOG FIRED, PC ", hex(cpu.getPC())); // TODO: alert on video - this.reset(); - } - // NMI interrupt @ 0x66 - if (interruptEnabled) { cpu.NMI(); } - return steps; - } - - getRasterScanline() { return this.scanline; } - - loadROM(title, data) { - rom.set(padBytes(data, romSize)); - - for (var i = 0; i < 32; i++) { - var b = rom[palBase + i]; - palette[i] = 0xff000000; - for (var j = 0; j < 8; j++) - if (((1 << j) & b)) - palette[i] += bitcolors[j]; - } - - this.reset(); - } - - loadState(state) { - cpu.loadState(state.c); - ram.mem.set(state.b); - vram.mem.set(state.bv); - oram.mem.set(state.bo); - watchdog_counter = state.wdc; - interruptEnabled = state.ie; - gfx.starsEnabled = state.se; - gfx.frameCounter = state.fc; - inputs[0] = state.in0; - inputs[1] = state.in1; - inputs[2] = state.in2; - } - saveState() { - return { - c: this.getCPUState(), - b: ram.mem.slice(0), - bv: vram.mem.slice(0), - bo: oram.mem.slice(0), - fc: gfx.frameCounter, - ie: interruptEnabled, - se: gfx.starsEnabled, - wdc: watchdog_counter, - in0: inputs[0], - in1: inputs[1], - in2: inputs[2], - }; - } - loadControlsState(state) { - inputs[0] = state.in0; - inputs[1] = state.in1; - inputs[2] = state.in2; - } - saveControlsState() { - return { - in0: inputs[0], - in1: inputs[1], - in2: inputs[2], - }; - } - getCPUState() { - return cpu.saveState(); - } - - isRunning() { - return timer && timer.isRunning(); - } - pause() { - timer.stop(); - audio.stop(); - } - resume() { - timer.start(); - audio.start(); - } - reset() { - cpu.reset(); - watchdog_counter = INITIAL_WATCHDOG; - } - getMemoryMap = function() { return { main:[ - {name:'Video RAM',start:0x5000,size:0x400,type:'ram'}, - {name:'Sprite RAM',start:0x5800,size:0x100,type:'ram'}, - {name:'I/O Registers',start:0x6000,size:0x2000,type:'io'}, - ] } }; - } - - return new GalaxianPlatform(); -} - -const _GalaxianScramblePlatform = function(mainElement) { - return _GalaxianPlatform(mainElement, { - romSize: 0x5020, - gfxBase: 0x4000, - palBase: 0x5000, - scramble: true, - keyMap: SCRAMBLE_KEYCODE_MAP, - missileWidth: 1, - missileOffset: 6, - }); -} - -PLATFORMS['galaxian'] = _GalaxianPlatform; -PLATFORMS['galaxian-scramble'] = _GalaxianScramblePlatform; +PLATFORMS['galaxian'] = GalaxianPlatform; +PLATFORMS['galaxian-scramble'] = GalaxianScramblePlatform; diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index 7d26971c..dddcf66b 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -48,6 +48,7 @@ var _atari8 = require('gen/platform/atari8.js'); var _atari7800 = require('gen/platform/atari7800.js'); var _coleco = require('gen/platform/coleco.js'); var _sms = require('gen/platform/sms.js'); +var _c64 = require('gen/platform/c64.js'); // @@ -301,4 +302,14 @@ describe('Platform Replay', () => { assert.equal(0x1800, platform.saveState().maria.dll); assert.equal(39, platform.readAddress(0x81)); // player y pos }); + /* TODO + it('Should run c64', () => { + var platform = testPlatform('c64', 'sprites.dasm.rom', 92, (platform, frameno) => { + if (frameno == 62) { + keycallback(Keys.VK_DOWN.c, Keys.VK_DOWN.c, 1); + } + }); + assert.equal(39, platform.readAddress(0x81)); // player y pos + }); + */ });