8bitworkshop/src/machine/atari8.ts

542 lines
17 KiB
TypeScript

import { newPOKEYAudio, TssChannelAdapter } from "../common/audio";
import { Machine } from "../common/baseplatform";
import { MOS6502 } from "../common/cpu/MOS6502";
import { AcceptsKeyInput, AcceptsPaddleInput, AcceptsROM, BasicScanlineMachine, FrameBased, Probeable, TrapCondition, VideoSource } from "../common/devices";
import { KeyFlags, Keys, makeKeycodeMap, newAddressDecoder, newKeyboardHandler } from "../common/emu";
import { hex } from "../common/util";
import { BaseWASIMachine } from "../common/wasmplatform";
import { ANTIC, MODE_SHIFT } from "./chips/antic";
import { CONSOL, GTIA, TRIG0 } from "./chips/gtia";
import { POKEY } from "./chips/pokey";
const ATARI8_KEYMATRIX_INTL_NOSHIFT = [
Keys.VK_L, Keys.VK_J, Keys.VK_SEMICOLON, Keys.VK_F4, Keys.VK_F5, Keys.VK_K, Keys.VK_BACK_SLASH, Keys.VK_TILDE,
Keys.VK_O, null, Keys.VK_P, Keys.VK_U, Keys.VK_ENTER, Keys.VK_I, Keys.VK_MINUS2, Keys.VK_EQUALS2,
Keys.VK_V, Keys.VK_F7, Keys.VK_C, Keys.VK_F6, Keys.VK_F4, Keys.VK_B, Keys.VK_X, Keys.VK_Z,
Keys.VK_4, null, Keys.VK_3, Keys.VK_6, Keys.VK_ESCAPE, Keys.VK_5, Keys.VK_2, Keys.VK_1,
Keys.VK_COMMA, Keys.VK_SPACE, Keys.VK_PERIOD, Keys.VK_N, null, Keys.VK_M, Keys.VK_SLASH, null/*invert*/,
Keys.VK_R, null, Keys.VK_E, Keys.VK_Y, Keys.VK_TAB, Keys.VK_T, Keys.VK_W, Keys.VK_Q,
Keys.VK_9, null, Keys.VK_0, Keys.VK_7, Keys.VK_BACK_SPACE, Keys.VK_8, null, null,
Keys.VK_F, Keys.VK_H, Keys.VK_D, null, Keys.VK_CAPS_LOCK, Keys.VK_G, Keys.VK_S, Keys.VK_A,
];
//TODO
var ATARI8_KEYCODE_MAP = makeKeycodeMap([
[Keys.UP, 0, 0x1],
[Keys.DOWN, 0, 0x2],
[Keys.LEFT, 0, 0x4],
[Keys.RIGHT, 0, 0x8],
[{ c: 16, n: "Shift", plyr: 0, button: 0 }, 2, 0x1],
/*
[Keys.P2_UP, 0, 0x10],
[Keys.P2_DOWN, 0, 0x20],
[Keys.P2_LEFT, 0, 0x40],
[Keys.P2_RIGHT, 0, 0x80],
[Keys.P2_A, 3, 0x1],
*/
[Keys.VK_F1, 3, 0x1], // START
[Keys.VK_F2, 3, 0x2], // SELECT
[Keys.VK_F3, 3, 0x4], // OPTION
]);
export class Atari800 extends BasicScanlineMachine {
// http://www.ataripreservation.org/websites/freddy.offenga/megazine/ISSUE5-PALNTSC.html
cpuFrequency = 1789773;
numTotalScanlines = 262;
cpuCyclesPerLine = 114;
canvasWidth = 336;
numVisibleScanlines = 224;
aspectRatio = this.canvasWidth / this.numVisibleScanlines * 0.857;
firstVisibleScanline = 16;
firstVisibleClock = (44 - 6) * 2; // ... to 215 * 2
defaultROMSize = 0x8000;
overscan = true;
audioOversample = 2;
sampleRate = this.numTotalScanlines * 60 * this.audioOversample;
run_address = -1;
cpu: MOS6502;
ram: Uint8Array;
bios: Uint8Array;
bus;
audio_pokey;
audioadapter;
antic: ANTIC;
gtia: GTIA;
irq_pokey: POKEY;
inputs = new Uint8Array(4);
linergb = new Uint32Array(this.canvasWidth);
lastdmabyte = 0;
keycode = 0;
cart_80 = false;
cart_a0 = false;
xexdata = null;
keyboard_active = true;
d500 = new Uint8Array(0x100);
// TODO: save/load vars
constructor() {
super();
this.cpu = new MOS6502();
this.ram = new Uint8Array(0x10000);
this.bios = new Uint8Array(0x2800);
this.bus = this.newBus();
this.connectCPUMemoryBus(this.bus);
// create support chips
this.antic = new ANTIC(this.readDMA.bind(this), this.antic_nmi.bind(this));
this.gtia = new GTIA();
this.irq_pokey = new POKEY(this.pokey_irq.bind(this), () => this.antic.h);
this.audio_pokey = newPOKEYAudio(1);
this.audioadapter = new TssChannelAdapter(this.audio_pokey.pokey1, this.audioOversample, this.sampleRate);
this.handler = newKeyboardHandler(
this.inputs, ATARI8_KEYCODE_MAP, this.getKeyboardFunction(), true);
}
newBus() {
return {
read: newAddressDecoder([
[0x0000, 0x7fff, 0xffff, (a) => { return this.ram[a]; }],
[0x8000, 0x9fff, 0xffff, (a) => { return this.cart_80 ? this.rom[a - 0x8000] : this.ram[a]; }],
[0xa000, 0xbfff, 0xffff, (a) => { return this.cart_a0 ? this.rom[a - 0x8000] : this.ram[a]; }],
[0xd000, 0xd0ff, 0x1f, (a) => { return this.gtia.readReg(a); }],
[0xd200, 0xd2ff, 0xf, (a) => { return this.readPokey(a); }],
[0xd300, 0xd3ff, 0xf, (a) => { return this.readPIA(a); }],
[0xd400, 0xd4ff, 0xf, (a) => { return this.antic.readReg(a); }],
[0xd500, 0xd5ff, 0xff, (a) => { return this.d500[a]; }],
[0xd800, 0xffff, 0xffff, (a) => { return this.bios[a - 0xd800]; }],
]),
write: newAddressDecoder([
[0x0000, 0xbffa, 0xffff, (a, v) => { this.ram[a] = v; }],
[0xbffb, 0xbfff, 0xffff, (a, v) => { this.ram[a] = v; this.initCartA(); }],
[0xd000, 0xd0ff, 0x1f, (a, v) => { this.gtia.setReg(a, v); }],
[0xd200, 0xd2ff, 0xf, (a, v) => { this.writePokey(a, v); }],
[0xd400, 0xd4ff, 0xf, (a, v) => { this.antic.setReg(a, v); }],
[0xd500, 0xd5ff, 0xff, (a, v) => { this.writeMapper(a, v); }],
]),
};
}
loadBIOS(bios: Uint8Array) {
this.bios.set(bios);
}
reset() {
super.reset();
this.antic.reset();
this.gtia.reset();
this.keycode = 0;
//if (this.xexdata) this.cart_a0 = true; // TODO
}
read(a) {
// TODO: lastdmabyte?
return this.bus.read(a);
}
// used by ANTIC
readDMA(a) {
let v = this.bus.read(a);
this.probe.logDMARead(a, v);
this.lastdmabyte = v;
return v;
}
readConst(a) {
return a < 0xd000 || a >= 0xd500 ? this.bus.read(a) : 0xff;
}
write(a, v) {
this.bus.write(a, v);
}
readPokey(a: number) {
switch (a & 0xf) {
case 9: // KBCODE
return this.keycode & 0xff;
case 15: // SKSTAT
return ((~this.keycode >> 6) & 0x4) | ((~this.keycode >> 3) & 0x8) | 0x12;
default:
return this.irq_pokey.read(a);
}
}
readPIA(a: number) {
if (a == 0 || a == 1) { return ~this.inputs[a]; }
}
writePokey(a, v) {
this.audio_pokey.pokey1.setRegister(a, v);
this.irq_pokey.write(a, v);
}
startScanline() {
// TODO: if (this.antic.h != 0) throw new Error(this.antic.h+"");
//if (this.cpu.isHalted()) throw new EmuHalt("CPU HALTED");
// set GTIA switch inputs
this.gtia.sync();
// TODO: trigger latching mode
for (let i = 0; i < 4; i++)
this.gtia.readregs[TRIG0 + i] = (~this.inputs[2] >> i) & 1;
// console switches
this.gtia.readregs[CONSOL] = ~this.inputs[3] & 0x7;
// advance POKEY audio
this.audio && this.audioadapter.generate(this.audio);
// advance POKEY IRQ timers
this.irq_pokey.advanceScanline();
}
drawScanline() {
// TODO
let y = this.antic.v - this.firstVisibleScanline;
if (y >= 0 && y < this.numVisibleScanlines) {
this.pixels.set(this.linergb, y * this.canvasWidth);
}
}
advanceCPU(): number {
// update ANTIC
if (this.antic.clockPulse()) {
// ANTIC DMA cycle, update GTIA
if (this.antic.h < 8)
this.gtia.updateGfx(this.antic.h - 1, this.antic.v, this.lastdmabyte); // HALT pin
if (this.antic.isWSYNC())
this.probe.logWait(0);
this.probe.logClocks(1);
} else {
super.advanceCPU();
}
// update GTIA
// get X coordinate within scanline
let xofs = this.antic.h * 4 - this.firstVisibleClock;
// GTIA tick functions
let gtiatick1 = () => {
this.gtia.clockPulse1();
this.linergb[xofs++] = this.gtia.rgb;
}
let gtiatick2 = () => {
this.gtia.clockPulse2();
this.linergb[xofs++] = this.gtia.rgb;
}
// tick 4 GTIA clocks for each CPU/ANTIC cycle
this.gtia.clockPulse4();
// correct for HSCROL -- bias antic +2, bias gtia -1
if ((this.antic.dliop & 0x10) && (this.antic.regs[4] & 1)) {
xofs += 2;
this.gtia.setBias(-1);
} else {
this.gtia.setBias(0);
}
let bp = MODE_SHIFT[this.antic.mode];
let odd = this.antic.h & 1;
if (bp < 8 || odd) { this.gtia.an = this.antic.shiftout(); }
gtiatick1();
if (bp == 1) { this.gtia.an = this.antic.shiftout(); }
gtiatick2();
if (bp <= 2) { this.gtia.an = this.antic.shiftout(); }
gtiatick1();
if (bp == 1) { this.gtia.an = this.antic.shiftout(); }
gtiatick2();
return 1;
}
loadState(state: any) {
this.loadControlsState(state);
this.cpu.loadState(state.c);
this.ram.set(state.ram);
this.antic.loadState(state.antic);
this.gtia.loadState(state.gtia);
this.irq_pokey.loadState(state.pokey);
this.lastdmabyte = state.lastdmabyte;
this.cart_80 = state.cart_80;
this.cart_a0 = state.cart_a0;
}
saveState() {
return {
c: this.cpu.saveState(),
ram: this.ram.slice(0),
antic: this.antic.saveState(),
gtia: this.gtia.saveState(),
pokey: this.irq_pokey.saveState(),
inputs: this.inputs.slice(0),
lastdmabyte: this.lastdmabyte,
keycode: this.keycode,
cart_80: this.cart_80,
cart_a0: this.cart_a0,
};
}
loadControlsState(state) {
this.inputs.set(state.inputs);
this.keycode = state.keycode;
}
saveControlsState() {
return {
inputs: this.inputs.slice(0),
keycode: this.keycode,
};
}
getRasterScanline() {
return this.antic.v;
}
getRasterLineClock() {
return this.antic.h;
}
getDebugCategories() {
return ['CPU', 'Stack', 'ANTIC', 'GTIA', 'POKEY'];
}
getDebugInfo(category, state) {
switch (category) {
case 'ANTIC': return ANTIC.stateToLongString(state.antic);
case 'GTIA': return GTIA.stateToLongString(state.gtia);
case 'POKEY': return POKEY.stateToLongString(state.pokey);
}
}
getKeyboardFunction() {
return (o, key, code, flags) => {
if (!this.keyboard_active) return false;
if (flags & (KeyFlags.KeyDown | KeyFlags.KeyUp)) {
//console.log(o, key, code, flags, hex(this.keycode));
var keymap = ATARI8_KEYMATRIX_INTL_NOSHIFT;
if (key == Keys.VK_F9.c) {
this.irq_pokey.generateIRQ(0x80); // break IRQ
return true;
}
for (var i = 0; i < keymap.length; i++) {
if (keymap[i] && keymap[i].c == key) {
this.keycode = i;
if (flags & KeyFlags.Shift) { this.keycode |= 0x40; }
if (flags & KeyFlags.Ctrl) { this.keycode |= 0x80; }
if (flags & KeyFlags.KeyDown) {
this.keycode |= 0x100;
this.irq_pokey.generateIRQ(0x40); // key pressed IRQ
return true;
}
}
}
};
}
}
pokey_irq() {
this.cpu.IRQ();
this.probe.logInterrupt(2);
}
antic_nmi() {
this.cpu.NMI();
this.probe.logInterrupt(1);
}
loadROM(rom: Uint8Array, title: string) {
if ((rom[0] == 0xff && rom[1] == 0xff) && !title?.endsWith('.rom')) {
// XEX file, chill out and wait for BIOS hook
this.xexdata = rom;
} else {
this.loadCartridge(rom);
}
}
loadCartridge(rom: Uint8Array) {
// TODO: https://github.com/dmlloyd/atari800/blob/master/DOC/cart.txt
// strip off header
if (rom[0] == 0x43 && rom[1] == 0x41 && rom[2] == 0x52 && rom[3] == 0x54) {
rom = rom.slice(16);
}
if (rom.length != 0x1000 && rom.length != 0x2000 && rom.length != 0x4000 && rom.length != 0x8000)
throw new Error("Sorry, this platform can only load 4/8/16/32 KB cartridges at the moment.");
// TODO: support other than 8 KB carts
// support 4/8/16/32 KB carts
let rom2 = new Uint8Array(0x8000);
for (let i = 0; i <= rom2.length - rom.length; i += rom.length) {
rom2.set(rom, i);
}
this.run_address = rom2[0x7ffe] + rom2[0x7fff]*256;
this.cart_a0 = true; // TODO
this.cart_80 = rom.length == 0x4000;
super.loadROM(rom2);
}
writeMapper(addr: number, value: number) {
// TODO
if (addr == 0xff) {
if (value == 0x80) this.cart_80 = false;
if (value == 0xa0) this.cart_a0 = false;
}
}
loadXEX(rom: Uint8Array) {
let ofs = 2;
let stub = this.d500;
let stubofs = 0; // stub routine
var runaddr = -1;
// load segments into RAM
while (ofs < rom.length) {
let start = rom[ofs + 0] + rom[ofs + 1] * 256;
let end = rom[ofs + 2] + rom[ofs + 3] * 256;
console.log('XEX', hex(ofs), hex(start), hex(end));
ofs += 4;
for (let i = start; i <= end; i++) {
this.ram[i] = rom[ofs++];
}
if (start == 0x2e0 && end == 0x2e1) {
runaddr = this.ram[0x2e0] + this.ram[0x2e1] * 256;
console.log('XEX run', hex(runaddr));
}
if (start == 0x2e2 && end == 0x2e3) {
var initaddr = this.ram[0x2e2] + this.ram[0x2e3] * 256;
console.log('XEX init', hex(initaddr));
stub[stubofs++] = 0x20;
stub[stubofs++] = initaddr & 0xff;
stub[stubofs++] = initaddr >> 8;
}
if (ofs > rom.length) throw new Error("Bad .XEX file format");
}
if (runaddr >= 0) {
// build stub routine at 0xd500
stub[stubofs++] = 0xa9; // lda #$a0
stub[stubofs++] = 0xa0;
stub[stubofs++] = 0x8d; // sta $d5ff (disable cart)
stub[stubofs++] = 0xff;
stub[stubofs++] = 0xd5;
stub[stubofs++] = 0x4c; // jmp runaddr
stub[stubofs++] = runaddr & 0xff;
stub[stubofs++] = runaddr >> 8;
// set DOSVEC to 0xd500
this.ram[0xa] = 0x00;
this.ram[0xb] = 0xd5;
this.run_address = 0xd500;
}
}
initCartA() {
if (this.cpu.getPC() == 0xf17f && this.xexdata) {
this.loadXEX(this.xexdata);
}
}
}
export class Atari5200 extends Atari800 {
newBus() {
return {
read: newAddressDecoder([
[0x0000, 0x3fff, 0xffff, (a) => { return this.ram[a]; }],
[0x4000, 0xbfff, 0xffff, (a) => { return this.rom ? this.rom[a - 0x4000] : 0; }],
[0xc000, 0xcfff, 0x1f, (a) => { return this.gtia.readReg(a); }],
[0xd400, 0xd4ff, 0xf, (a) => { return this.antic.readReg(a); }],
[0xe800, 0xefff, 0xf, (a) => { return this.readPokey(a); }],
[0xf800, 0xffff, 0x7ff, (a) => { return this.bios[a]; }],
]),
write: newAddressDecoder([
[0x0000, 0x3fff, 0xffff, (a, v) => { this.ram[a] = v; }],
[0xc000, 0xcfff, 0x1f, (a, v) => { this.gtia.setReg(a, v); }],
[0xd400, 0xd4ff, 0xf, (a, v) => { this.antic.setReg(a, v); }],
[0xe800, 0xefff, 0xf, (a, v) => { this.writePokey(a, v); }],
]),
};
}
}
///
export class Atari8_WASMMachine extends BaseWASIMachine
implements Machine, Probeable, VideoSource, AcceptsROM, FrameBased, AcceptsKeyInput, AcceptsPaddleInput {
numTotalScanlines = 312;
cpuCyclesPerLine = 63;
prgstart: number;
joymask0 = 0;
joymask1 = 0;
loadROM(rom: Uint8Array) {
super.loadROM(rom);
this.reloadROM();
}
reloadROM() {
if (this.sys) {
var result = this.exports.machine_load_rom(this.sys, this.romptr, this.romlen);
console.log('machine_load_rom', result);
//console.log(this.wasmFs.fs.existsSync('atari8.img'), result);
}
}
loadBIOS(srcArray: Uint8Array) {
super.loadBIOS(srcArray);
}
reset() {
this.reloadROM();
}
advanceFrame(trap: TrapCondition): number {
// TODO
this.exports.machine_start_frame(this.sys);
if (trap) {
this.advanceFrameClock(trap, 999999); // TODO?
} else {
this.exports.machine_advance_frame(this.sys);
}
this.syncVideo();
this.syncAudio();
return 1;
}
getCPUState() {
this.exports.machine_save_cpu_state(this.sys, this.stateptr);
var s = this.statearr;
var pc = s[6] + (s[7] << 8);
return {
PC: pc,
SP: s[2],
A: s[0],
X: s[3],
Y: s[4],
C: s[1] & 1,
Z: s[1] & 2,
I: s[1] & 4,
D: s[1] & 8,
V: s[1] & 64,
N: s[1] & 128,
o: this.readConst(pc),
}
}
saveState() {
var cpu = this.getCPUState();
this.exports.machine_save_state(this.sys, this.stateptr);
return {
c: cpu,
state: this.statearr.slice(0),
//ram:this.statearr.slice(18640, 18640+0x200), // ZP and stack
};
}
loadState(state): void {
this.statearr.set(state.state);
this.exports.machine_load_state(this.sys, this.stateptr);
}
getVideoParams() {
return { width: 384, height: 240, overscan: true, videoFrequency: 60 };
}
pollControls() {
}
setKeyInput(key: number, code: number, flags: number): void {
// modifier flags
if (flags & KeyFlags.Shift) key |= 0x100;
if (flags & KeyFlags.Ctrl) key |= 0x200;
// keyboard -> joystick
var mask = 0;
if (key == 37) { key = 0x8; mask = 0x4; } // LEFT
if (key == 38) { key = 0xb; mask = 0x1; } // UP
if (key == 39) { key = 0x9; mask = 0x8; } // RIGHT
if (key == 40) { key = 0xa; mask = 0x2; } // DOWN
if (key == 32) { mask = 0x100; } // FIRE
// set machine inputs
if (flags & KeyFlags.KeyDown) {
this.exports.machine_key_down(this.sys, key);
this.joymask0 |= mask;
} else if (flags & KeyFlags.KeyUp) {
this.exports.machine_key_up(this.sys, key);
this.joymask0 &= ~mask;
}
this.setJoyInput(0, this.joymask0);
this.setJoyInput(1, this.joymask1);
}
setJoyInput(joy: number, mask: number) {
this.exports.machine_joy_set(this.sys, joy, mask);
}
setPaddleInput(controller: number, value: number): void {
this.exports.machine_paddle_set(this.sys, controller, value);
}
}