8bitworkshop/src/machine/apple2.ts

1382 lines
47 KiB
TypeScript

import { MOS6502, MOS6502State } from "../common/cpu/MOS6502";
import { Bus, BasicScanlineMachine, xorshift32, SavesState } from "../common/devices";
import { KeyFlags } from "../common/emu"; // TODO
import { hex, lzgmini, stringToByteArray, RGBA, printFlags } from "../common/util";
const cpuFrequency = 1023000;
const cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/
const cpuCyclesPerFrame = 65*262;
// TODO: read prodos/ca65 header?
const VM_BASE = 0x803; // where to JMP after pr#6
const LOAD_BASE = VM_BASE;
const PGM_BASE = VM_BASE;
const HDR_SIZE = PGM_BASE - LOAD_BASE;
interface AppleIIStateBase {
ram : Uint8Array;
rnd,soundstate : number;
auxRAMselected,writeinhibit : boolean;
auxRAMbank : number;
}
interface AppleIIControlsState {
inputs : Uint8Array; // unused?
kbdlatch : number;
}
interface AppleIIState extends AppleIIStateBase, AppleIIControlsState {
c : MOS6502State;
grswitch : number;
slots: SlotDevice[];
}
interface SlotDevice extends Bus {
readROM(address: number) : number;
readConst(address: number) : number;
}
export class AppleII extends BasicScanlineMachine {
cpuFrequency = 1023000;
sampleRate = this.cpuFrequency;
cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/
cpuCyclesPerFrame = 65*262;
canvasWidth = 280;
numVisibleScanlines = 192;
numTotalScanlines = 262;
defaultROMSize = 0xbf00-0x803; // TODO
ram = new Uint8Array(0x13000); // 64K + 16K LC RAM - 4K hardware + 12K ROM
bios : Uint8Array;
cpu = new MOS6502();
grdirty = new Array(0xc000 >> 7);
grparams = {dirty:this.grdirty, grswitch:GR_TXMODE, mem:this.ram};
ap2disp;
rnd = 1;
kbdlatch = 0;
soundstate = 0;
// language card switches
auxRAMselected = false;
auxRAMbank = 1;
writeinhibit = true;
// value to add when reading & writing each of these banks
// bank 1 is E000-FFFF, bank 2 is D000-DFFF
bank2rdoffset=0;
bank2wroffset=0;
// disk II
slots : SlotDevice[] = new Array(8);
// fake disk drive that loads program into RAM
fakeDrive : SlotDevice = {
readROM: (a) => {
switch (a) {
// JMP VM_BASE
case 0: {
// load program into RAM
if (this.rom)
this.ram.set(this.rom.slice(HDR_SIZE), PGM_BASE);
return 0x4c;
}
case 1: return VM_BASE&0xff;
case 2: return (VM_BASE>>8)&0xff;
default: return 0;
}
},
readConst: (a) => {
return 0;
},
read: (a) => { return this.noise(); },
write: (a,v) => { }
};
constructor() {
super();
this.bios = new lzgmini().decode(stringToByteArray(atob(APPLEIIGO_LZG)));
this.bios[0x39a] = 0x60; // $d39a = RTS
this.ram.set(this.bios, 0xd000);
this.ram[0xbf00] = 0x4c; // fake DOS detect for C
this.ram[0xbf6f] = 0x01; // fake DOS detect for C
this.connectCPUMemoryBus(this);
}
saveState() : AppleIIState {
// TODO: automagic
return {
c: this.cpu.saveState(),
ram: this.ram.slice(),
rnd: this.rnd,
kbdlatch: this.kbdlatch,
soundstate: this.soundstate,
grswitch: this.grparams.grswitch,
auxRAMselected: this.auxRAMselected,
auxRAMbank: this.auxRAMbank,
writeinhibit: this.writeinhibit,
slots: this.slots.map((slot) => { return slot && slot['saveState'] && slot['saveState']() }),
inputs: null
};
}
loadState(s:AppleIIState) {
this.cpu.loadState(s.c);
this.ram.set(s.ram);
this.rnd = s.rnd;
this.kbdlatch = s.kbdlatch;
this.soundstate = s.soundstate;
this.grparams.grswitch = s.grswitch;
this.auxRAMselected = s.auxRAMselected;
this.auxRAMbank = s.auxRAMbank;
this.writeinhibit = s.writeinhibit;
this.setupLanguageCardConstants();
for (var i=0; i<this.slots.length; i++)
if (this.slots[i] && this.slots[i]['loadState'])
this.slots[i]['loadState'](s.slots[i]);
this.ap2disp.invalidate(); // repaint entire screen
}
saveControlsState() : AppleIIControlsState {
return {inputs:null,kbdlatch:this.kbdlatch};
}
loadControlsState(s:AppleIIControlsState) {
this.kbdlatch = s.kbdlatch;
}
loadROM(data) {
if (data.length == 35*16*256) { // is it a disk image?
var diskii = new DiskII(this, data);
this.slots[6] = diskii;
} else { // it's a binary, use a fake drive
super.loadROM(data);
this.slots[6] = this.fakeDrive;
}
}
reset() {
super.reset();
this.rnd = 1;
this.auxRAMselected = false;
this.auxRAMbank = 1;
this.writeinhibit = true;
this.skipboot();
}
skipboot() {
// execute until $c600 boot
for (var i=0; i<2000000; i++) {
this.cpu.advanceClock();
if ((this.cpu.getPC()>>8) == 0xc6) break;
}
// get out of $c600 boot
for (var i=0; i<2000000; i++) {
this.cpu.advanceClock();
if ((this.cpu.getPC()>>8) < 0xc6) break;
}
}
noise() : number {
return (this.rnd = xorshift32(this.rnd)) & 0xff;
}
readConst(address: number): number {
if (address < 0xc000) {
return this.ram[address];
} else if (address >= 0xd000) {
if (!this.auxRAMselected)
return this.bios[address - 0xd000];
else if (address >= 0xe000)
return this.ram[address];
else
return this.ram[address + this.bank2rdoffset];
} else if (address >= 0xc100 && address < 0xc800) {
var slot = (address >> 8) & 7;
return (this.slots[slot] && this.slots[slot].readConst(address & 0xff)) | 0;
} else {
return 0;
}
}
read(address:number) : number {
address &= 0xffff;
if (address < 0xc000 || address >= 0xd000) {
return this.readConst(address);
} else if (address < 0xc100) {
var slot = (address >> 4) & 0x0f;
switch (slot)
{
case 0:
return this.kbdlatch;
case 1:
this.kbdlatch &= 0x7f;
break;
case 3:
this.soundstate = this.soundstate ^ 1;
break;
case 5:
if ((address & 0x0f) < 8) {
// graphics
if ((address & 1) != 0)
this.grparams.grswitch |= 1 << ((address >> 1) & 0x07);
else
this.grparams.grswitch &= ~(1 << ((address >> 1) & 0x07));
}
break;
case 6:
// tapein, joystick, buttons
switch (address & 7) {
// buttons (off)
case 1:
case 2:
case 3:
return this.noise() & 0x7f;
// joystick
case 4:
case 5:
return this.noise() | 0x80;
default:
return this.noise();
}
case 7:
// joy reset
if (address == 0xc070)
return this.noise() | 0x80;
case 8:
return this.doLanguageCardIO(address);
case 9: case 10: case 11: case 12: case 13: case 14: case 15:
return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0;
}
} else if (address >= 0xc100 && address < 0xc800) {
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 {
address &= 0xffff;
val &= 0xff;
if (address < 0xc000) {
this.ram[address] = val;
this.grdirty[address>>7] = 1;
} else if (address < 0xc080) {
this.read(address); // strobe address, discard result
} else if (address < 0xc100) {
var slot = (address >> 4) & 0x0f;
this.slots[slot-8] && this.slots[slot-8].write(address & 0xf, val);
} else if (address >= 0xd000 && !this.writeinhibit) {
if (address >= 0xe000)
this.ram[address] = val;
else
this.ram[address + this.bank2wroffset] = val;
}
}
connectVideo(pixels:Uint32Array) {
super.connectVideo(pixels);
this.ap2disp = this.pixels && new Apple2Display(this.pixels, this.grparams);
}
startScanline() {
}
drawScanline() {
// TODO: draw scanline via ap2disp
}
advanceFrame(trap) : number {
var clocks = super.advanceFrame(trap);
this.ap2disp && this.ap2disp.updateScreen();
return clocks;
}
advanceCPU() {
this.audio.feedSample(this.soundstate, 1);
return super.advanceCPU();
}
setKeyInput(key:number, code:number, flags:number) : void {
if (flags & KeyFlags.KeyPress) {
// convert to uppercase for Apple ][
if (code >= 0x61 && code <= 0x7a)
code -= 32;
if (code >= 32) {
if (code >= 65 && code < 65+26) {
if (flags & KeyFlags.Ctrl)
code -= 64; // ctrl
}
this.kbdlatch = (code | 0x80) & 0xff;
}
} else if (flags & KeyFlags.KeyDown) {
code = 0;
switch (key) {
case 8: code=8; break; // left
case 13: code=13; break; // return
case 27: code=27; break; // escape
case 37: code=8; break; // left
case 39: code=21; break; // right
case 38: code=11; break; // up
case 40: code=10; break; // down
}
if (code)
this.kbdlatch = (code | 0x80) & 0xff;
}
}
doLanguageCardIO(address:number) {
switch (address & 0x0f) {
// Select aux RAM bank 2, write protected.
case 0x0:
case 0x4:
this.auxRAMselected = true;
this.auxRAMbank = 2;
this.writeinhibit = true;
break;
// Select ROM, write enable aux RAM bank 2.
case 0x1:
case 0x5:
this.auxRAMselected = false;
this.auxRAMbank = 2;
this.writeinhibit = false;
break;
// Select ROM, write protect aux RAM (either bank).
case 0x2:
case 0x6:
case 0xA:
case 0xE:
this.auxRAMselected = false;
this.writeinhibit = true;
break;
// Select aux RAM bank 2, write enabled.
case 0x3:
case 0x7:
this.auxRAMselected = true;
this.auxRAMbank = 2;
this.writeinhibit = false;
break;
// Select aux RAM bank 1, write protected.
case 0x8:
case 0xC:
this.auxRAMselected = true;
this.auxRAMbank = 1;
this.writeinhibit = true;
break;
// Select ROM, write enable aux RAM bank 1.
case 0x9:
case 0xD:
this.auxRAMselected = false;
this.auxRAMbank = 1;
this.writeinhibit = false;
break;
// Select aux RAM bank 1, write enabled.
case 0xB:
case 0xF:
this.auxRAMselected = true;
this.auxRAMbank = 1;
this.writeinhibit = false;
break;
}
this.setupLanguageCardConstants();
return this.noise();
}
setupLanguageCardConstants() {
// reset language card constants
if (this.auxRAMbank == 2)
this.bank2rdoffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff
else
this.bank2rdoffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff
if (this.auxRAMbank == 2)
this.bank2wroffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff
else
this.bank2wroffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff
}
getDebugCategories() {
return ['CPU','Stack','I/O','Disk'];
}
getDebugInfo(category:string, state:AppleIIState) {
switch (category) {
case 'I/O': return "AUX RAM Bank: " + state.auxRAMbank +
"\nAUX RAM Select: " + state.auxRAMselected +
"\nAUX RAM Write: " + !state.writeinhibit +
"\n\nGR Switches: " + printFlags(state.grswitch, ["Graphics","Mixed","Page2","Hires"], false) +
"\n";
case 'Disk': return (this.slots[6] && this.slots[6]['toLongString'] && this.slots[6]['toLongString']()) || "\n";
}
}
}
const GR_TXMODE = 1;
const GR_MIXMODE = 2;
const GR_PAGE1 = 4;
const GR_HIRES = 8;
type AppleGRParams = {dirty:boolean[], grswitch:number, mem:Uint8Array};
var Apple2Display = function(pixels : Uint32Array, apple : AppleGRParams) {
var XSIZE = 280;
var YSIZE = 192;
var PIXELON = 0xffffffff;
var PIXELOFF = 0xff000000;
var oldgrmode = -1;
var textbuf = new Array(40*24);
const flashInterval = 500;
// https://mrob.com/pub/xapple2/colors.html
const loresColor = [
RGBA(0, 0, 0),
RGBA(227, 30, 96),
RGBA(96, 78, 189),
RGBA(255, 68, 253),
RGBA(0, 163, 96),
RGBA(156, 156, 156),
RGBA(20, 207, 253),
RGBA(208, 195, 255),
RGBA(96, 114, 3),
RGBA(255, 106, 60),
RGBA(156, 156, 156),
RGBA(255, 160, 208),
RGBA(20, 245, 60),
RGBA(208, 221, 141),
RGBA(114, 255, 208),
RGBA(255, 255, 255)
];
const text_lut = [
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380,
0x028, 0x0a8, 0x128, 0x1a8, 0x228, 0x2a8, 0x328, 0x3a8,
0x050, 0x0d0, 0x150, 0x1d0, 0x250, 0x2d0, 0x350, 0x3d0
];
const hires_lut = [
0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00,
0x0080, 0x0480, 0x0880, 0x0c80, 0x1080, 0x1480, 0x1880, 0x1c80,
0x0100, 0x0500, 0x0900, 0x0d00, 0x1100, 0x1500, 0x1900, 0x1d00,
0x0180, 0x0580, 0x0980, 0x0d80, 0x1180, 0x1580, 0x1980, 0x1d80,
0x0200, 0x0600, 0x0a00, 0x0e00, 0x1200, 0x1600, 0x1a00, 0x1e00,
0x0280, 0x0680, 0x0a80, 0x0e80, 0x1280, 0x1680, 0x1a80, 0x1e80,
0x0300, 0x0700, 0x0b00, 0x0f00, 0x1300, 0x1700, 0x1b00, 0x1f00,
0x0380, 0x0780, 0x0b80, 0x0f80, 0x1380, 0x1780, 0x1b80, 0x1f80,
0x0028, 0x0428, 0x0828, 0x0c28, 0x1028, 0x1428, 0x1828, 0x1c28,
0x00a8, 0x04a8, 0x08a8, 0x0ca8, 0x10a8, 0x14a8, 0x18a8, 0x1ca8,
0x0128, 0x0528, 0x0928, 0x0d28, 0x1128, 0x1528, 0x1928, 0x1d28,
0x01a8, 0x05a8, 0x09a8, 0x0da8, 0x11a8, 0x15a8, 0x19a8, 0x1da8,
0x0228, 0x0628, 0x0a28, 0x0e28, 0x1228, 0x1628, 0x1a28, 0x1e28,
0x02a8, 0x06a8, 0x0aa8, 0x0ea8, 0x12a8, 0x16a8, 0x1aa8, 0x1ea8,
0x0328, 0x0728, 0x0b28, 0x0f28, 0x1328, 0x1728, 0x1b28, 0x1f28,
0x03a8, 0x07a8, 0x0ba8, 0x0fa8, 0x13a8, 0x17a8, 0x1ba8, 0x1fa8,
0x0050, 0x0450, 0x0850, 0x0c50, 0x1050, 0x1450, 0x1850, 0x1c50,
0x00d0, 0x04d0, 0x08d0, 0x0cd0, 0x10d0, 0x14d0, 0x18d0, 0x1cd0,
0x0150, 0x0550, 0x0950, 0x0d50, 0x1150, 0x1550, 0x1950, 0x1d50,
0x01d0, 0x05d0, 0x09d0, 0x0dd0, 0x11d0, 0x15d0, 0x19d0, 0x1dd0,
0x0250, 0x0650, 0x0a50, 0x0e50, 0x1250, 0x1650, 0x1a50, 0x1e50,
0x02d0, 0x06d0, 0x0ad0, 0x0ed0, 0x12d0, 0x16d0, 0x1ad0, 0x1ed0,
0x0350, 0x0750, 0x0b50, 0x0f50, 0x1350, 0x1750, 0x1b50, 0x1f50,
0x03d0, 0x07d0, 0x0bd0, 0x0fd0, 0x13d0, 0x17d0, 0x1bd0, 0x1fd0
];
var colors_lut;
/**
* This function makes the color lookup table for hires mode.
* We make a table of 1024 * 2 * 7 entries.
* Why? Because we assume each color byte has 10 bits
* (8 real bits + 1 on each side) and we need different colors
* for odd and even addresses (2) and each byte displays 7 pixels.
*/
{
colors_lut = new Array(256*4*2*7);
var i,j;
var c1,c2,c3 = 15;
var base = 0;
// go thru odd and even
for (j=0; j<2; j++)
{
// go thru 1024 values
for (var b1=0; b1<1024; b1++)
{
// see if the hi bit is set
if ((b1 & 0x80) == 0)
{
c1 = 3; c2 = 12; // purple & green
} else
{
c1 = 6; c2 = 9; // blue & orange
}
// make a value consisting of:
// the 8th bit, then bits 0-7, then the 9th bit
var b = ((b1 & 0x100) >> 8) | ((b1 & 0x7f) << 1) |
((b1 & 0x200) >> 1);
// go through each pixel
for (i=0; i<7; i++)
{
var c;
// is this pixel lit?
if (((2<<i)&b) != 0)
{
// are there pixels lit on both sides of this one?
if (((7<<i)&b) == (7<<i))
// yes, make it white
c = 15;
else
// no, choose color based on odd/even byte
// and odd/even pixel column
c = ((((j ^ i) & 1) == 0) ? c1 : c2);
} else
{
// are there pixels lit in the previous & next
// column but none in this?
if (((5<<i)&b) == (5<<i))
// color this pixel
c = ((((j ^ i) & 1) != 0) ? c1 : c2);
else
c = 0;
}
colors_lut[base] = loresColor[c];
base++;
}
}
}
}
function drawLoresChar(x, y, b)
{
var i,base,adr,c;
base = (y<<3)*XSIZE + x*7; //(x<<2) + (x<<1) + x
c = loresColor[b & 0x0f];
for (i=0; i<4; i++)
{
pixels[base] =
pixels[base+1] =
pixels[base+2] =
pixels[base+3] =
pixels[base+4] =
pixels[base+5] =
pixels[base+6] = c;
base += XSIZE;
}
c = loresColor[b >> 4];
for (i=0; i<4; i++)
{
pixels[base] =
pixels[base+1] =
pixels[base+2] =
pixels[base+3] =
pixels[base+4] =
pixels[base+5] =
pixels[base+6] = c;
base += XSIZE;
}
}
function drawTextChar(x, y, b, invert)
{
var base = (y<<3)*XSIZE + x*7; // (x<<2) + (x<<1) + x
var on,off;
if (invert)
{
on = PIXELOFF;
off = PIXELON;
} else
{
on = PIXELON;
off = PIXELOFF;
}
for (var yy=0; yy<8; yy++)
{
var chr = apple2_charset[(b<<3)+yy];
pixels[base] = ((chr & 64) > 0)?on:off;
pixels[base+1] = ((chr & 32) > 0)?on:off;
pixels[base+2] = ((chr & 16) > 0)?on:off;
pixels[base+3] = ((chr & 8) > 0)?on:off;
pixels[base+4] = ((chr & 4) > 0)?on:off;
pixels[base+5] = ((chr & 2) > 0)?on:off;
pixels[base+6] = ((chr & 1) > 0)?on:off;
base += XSIZE;
}
}
function drawHiresLines(y, maxy)
{
var yb = y*XSIZE;
for (; y < maxy; y++)
{
var base = hires_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x4000 : 0x2000);
if (!apple.dirty[base >> 7])
{
yb += XSIZE;
continue;
}
var c1, c2;
var b = 0;
var b1 = apple.mem[base] & 0xff;
for (var x1=0; x1<20; x1++)
{
var b2 = apple.mem[base+1] & 0xff;
var b3 = apple.mem[base+2] & 0xff;
var d1 = (((b&0x40)<<2) | b1 | b2<<9) & 0x3ff;
for (var i=0; i<7; i++)
pixels[yb+i] = colors_lut[d1*7+i];
var d2 = (((b1&0x40)<<2) | b2 | b3<<9) & 0x3ff;
for (var i=0; i<7; i++)
pixels[yb+7+i] = colors_lut[d2*7+7168+i];
yb += 14;
base += 2;
b = b2;
b1 = b3;
}
}
}
function drawLoresLine(y)
{
// get the base address of this line
var base = text_lut[y] +
(((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400);
// if (!dirty[base >> 7])
// return;
for (var x=0; x<40; x++)
{
var b = apple.mem[base+x] & 0xff;
// if the char. changed, draw it
if (b != textbuf[y*40+x])
{
drawLoresChar(x, y, b);
textbuf[y*40+x] = b;
}
}
}
function drawTextLine(y, flash)
{
// get the base address of this line
var base = text_lut[y] +
(((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400);
// if (!dirty[base >> 7])
// return;
for (var x=0; x<40; x++)
{
var b = apple.mem[base+x] & 0xff;
var invert;
// invert flash characters 1/2 of the time
if (b >= 0x80)
{
invert = false;
} else if (b >= 0x40)
{
invert = flash;
if (flash)
b -= 0x40;
else
b += 0x40;
} else
invert = true;
// if the char. changed, draw it
if (b != textbuf[y*40+x])
{
drawTextChar(x, y, b & 0x7f, invert);
textbuf[y*40+x] = b;
}
}
}
this.updateScreen = function(totalrepaint)
{
var y;
var flash = (new Date().getTime() % (flashInterval<<1)) > flashInterval;
// if graphics mode changed, repaint whole screen
if (apple.grswitch != oldgrmode)
{
oldgrmode = apple.grswitch;
totalrepaint = true;
}
if (totalrepaint)
{
// clear textbuf if in text mode
if ((apple.grswitch & GR_TXMODE) != 0 || (apple.grswitch & GR_MIXMODE) != 0)
{
for (y=0; y<24; y++)
for (var x=0; x<40; x++)
textbuf[y*40+x] = -1;
}
for (var i=0; i<apple.dirty.length; i++)
apple.dirty[i] = true;
}
// first, draw top part of window
if ((apple.grswitch & GR_TXMODE) != 0)
{
for (y=0; y<20; y++)
drawTextLine(y, flash);
} else
{
if ((apple.grswitch & GR_HIRES) != 0)
drawHiresLines(0, 160);
else
for (y=0; y<20; y++)
drawLoresLine(y);
}
// now do mixed part of window
if ((apple.grswitch & GR_TXMODE) != 0 || (apple.grswitch & GR_MIXMODE) != 0)
{
for (y=20; y<24; y++)
drawTextLine(y, flash);
} else
{
if ((apple.grswitch & GR_HIRES) != 0)
drawHiresLines(160, 192);
else
for (y=20; y<24; y++)
drawLoresLine(y);
}
for (var i=0; i<apple.dirty.length; i++)
apple.dirty[i] = false;
}
this.invalidate = function() {
oldgrmode = -1;
}
}
/*exported apple2_charset */
const apple2_charset = [
0x00,0x1c,0x22,0x2a,0x2e,0x2c,0x20,0x1e,
0x00,0x08,0x14,0x22,0x22,0x3e,0x22,0x22,
0x00,0x3c,0x22,0x22,0x3c,0x22,0x22,0x3c,
0x00,0x1c,0x22,0x20,0x20,0x20,0x22,0x1c,
0x00,0x3c,0x22,0x22,0x22,0x22,0x22,0x3c,
0x00,0x3e,0x20,0x20,0x3c,0x20,0x20,0x3e,
0x00,0x3e,0x20,0x20,0x3c,0x20,0x20,0x20,
0x00,0x1e,0x20,0x20,0x20,0x26,0x22,0x1e,
0x00,0x22,0x22,0x22,0x3e,0x22,0x22,0x22,
0x00,0x1c,0x08,0x08,0x08,0x08,0x08,0x1c,
0x00,0x02,0x02,0x02,0x02,0x02,0x22,0x1c,
0x00,0x22,0x24,0x28,0x30,0x28,0x24,0x22,
0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x3e,
0x00,0x22,0x36,0x2a,0x2a,0x22,0x22,0x22,
0x00,0x22,0x22,0x32,0x2a,0x26,0x22,0x22,
0x00,0x1c,0x22,0x22,0x22,0x22,0x22,0x1c,
0x00,0x3c,0x22,0x22,0x3c,0x20,0x20,0x20,
0x00,0x1c,0x22,0x22,0x22,0x2a,0x24,0x1a,
0x00,0x3c,0x22,0x22,0x3c,0x28,0x24,0x22,
0x00,0x1c,0x22,0x20,0x1c,0x02,0x22,0x1c,
0x00,0x3e,0x08,0x08,0x08,0x08,0x08,0x08,
0x00,0x22,0x22,0x22,0x22,0x22,0x22,0x1c,
0x00,0x22,0x22,0x22,0x22,0x22,0x14,0x08,
0x00,0x22,0x22,0x22,0x2a,0x2a,0x36,0x22,
0x00,0x22,0x22,0x14,0x08,0x14,0x22,0x22,
0x00,0x22,0x22,0x14,0x08,0x08,0x08,0x08,
0x00,0x3e,0x02,0x04,0x08,0x10,0x20,0x3e,
0x00,0x3e,0x30,0x30,0x30,0x30,0x30,0x3e,
0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x00,
0x00,0x3e,0x06,0x06,0x06,0x06,0x06,0x3e,
0x00,0x00,0x00,0x08,0x14,0x22,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x08,0x08,0x08,0x08,0x08,0x00,0x08,
0x00,0x14,0x14,0x14,0x00,0x00,0x00,0x00,
0x00,0x14,0x14,0x3e,0x14,0x3e,0x14,0x14,
0x00,0x08,0x1e,0x28,0x1c,0x0a,0x3c,0x08,
0x00,0x30,0x32,0x04,0x08,0x10,0x26,0x06,
0x00,0x10,0x28,0x28,0x10,0x2a,0x24,0x1a,
0x00,0x08,0x08,0x08,0x00,0x00,0x00,0x00,
0x00,0x08,0x10,0x20,0x20,0x20,0x10,0x08,
0x00,0x08,0x04,0x02,0x02,0x02,0x04,0x08,
0x00,0x08,0x2a,0x1c,0x08,0x1c,0x2a,0x08,
0x00,0x00,0x08,0x08,0x3e,0x08,0x08,0x00,
0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x10,
0x00,0x00,0x00,0x00,0x3e,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,
0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x00,
0x00,0x1c,0x22,0x26,0x2a,0x32,0x22,0x1c,
0x00,0x08,0x18,0x08,0x08,0x08,0x08,0x1c,
0x00,0x1c,0x22,0x02,0x0c,0x10,0x20,0x3e,
0x00,0x3e,0x02,0x04,0x0c,0x02,0x22,0x1c,
0x00,0x04,0x0c,0x14,0x24,0x3e,0x04,0x04,
0x00,0x3e,0x20,0x3c,0x02,0x02,0x22,0x1c,
0x00,0x0e,0x10,0x20,0x3c,0x22,0x22,0x1c,
0x00,0x3e,0x02,0x04,0x08,0x10,0x10,0x10,
0x00,0x1c,0x22,0x22,0x1c,0x22,0x22,0x1c,
0x00,0x1c,0x22,0x22,0x1e,0x02,0x04,0x38,
0x00,0x00,0x00,0x08,0x00,0x08,0x00,0x00,
0x00,0x00,0x00,0x08,0x00,0x08,0x08,0x10,
0x00,0x04,0x08,0x10,0x20,0x10,0x08,0x04,
0x00,0x00,0x00,0x3e,0x00,0x3e,0x00,0x00,
0x00,0x10,0x08,0x04,0x02,0x04,0x08,0x10,
0x00,0x1c,0x22,0x04,0x08,0x08,0x00,0x08,
0x80,0x9c,0xa2,0xaa,0xae,0xac,0xa0,0x9e,
0x80,0x88,0x94,0xa2,0xa2,0xbe,0xa2,0xa2,
0x80,0xbc,0xa2,0xa2,0xbc,0xa2,0xa2,0xbc,
0x80,0x9c,0xa2,0xa0,0xa0,0xa0,0xa2,0x9c,
0x80,0xbc,0xa2,0xa2,0xa2,0xa2,0xa2,0xbc,
0x80,0xbe,0xa0,0xa0,0xbc,0xa0,0xa0,0xbe,
0x80,0xbe,0xa0,0xa0,0xbc,0xa0,0xa0,0xa0,
0x80,0x9e,0xa0,0xa0,0xa0,0xa6,0xa2,0x9e,
0x80,0xa2,0xa2,0xa2,0xbe,0xa2,0xa2,0xa2,
0x80,0x9c,0x88,0x88,0x88,0x88,0x88,0x9c,
0x80,0x82,0x82,0x82,0x82,0x82,0xa2,0x9c,
0x80,0xa2,0xa4,0xa8,0xb0,0xa8,0xa4,0xa2,
0x80,0xa0,0xa0,0xa0,0xa0,0xa0,0xa0,0xbe,
0x80,0xa2,0xb6,0xaa,0xaa,0xa2,0xa2,0xa2,
0x80,0xa2,0xa2,0xb2,0xaa,0xa6,0xa2,0xa2,
0x80,0x9c,0xa2,0xa2,0xa2,0xa2,0xa2,0x9c,
0x80,0xbc,0xa2,0xa2,0xbc,0xa0,0xa0,0xa0,
0x80,0x9c,0xa2,0xa2,0xa2,0xaa,0xa4,0x9a,
0x80,0xbc,0xa2,0xa2,0xbc,0xa8,0xa4,0xa2,
0x80,0x9c,0xa2,0xa0,0x9c,0x82,0xa2,0x9c,
0x80,0xbe,0x88,0x88,0x88,0x88,0x88,0x88,
0x80,0xa2,0xa2,0xa2,0xa2,0xa2,0xa2,0x9c,
0x80,0xa2,0xa2,0xa2,0xa2,0xa2,0x94,0x88,
0x80,0xa2,0xa2,0xa2,0xaa,0xaa,0xb6,0xa2,
0x80,0xa2,0xa2,0x94,0x88,0x94,0xa2,0xa2,
0x80,0xa2,0xa2,0x94,0x88,0x88,0x88,0x88,
0x80,0xbe,0x82,0x84,0x88,0x90,0xa0,0xbe,
0x80,0xbe,0xb0,0xb0,0xb0,0xb0,0xb0,0xbe,
0x80,0x80,0xa0,0x90,0x88,0x84,0x82,0x80,
0x80,0xbe,0x86,0x86,0x86,0x86,0x86,0xbe,
0x80,0x80,0x80,0x88,0x94,0xa2,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xbe,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,
0x80,0x88,0x88,0x88,0x88,0x88,0x80,0x88,
0x80,0x94,0x94,0x94,0x80,0x80,0x80,0x80,
0x80,0x94,0x94,0xbe,0x94,0xbe,0x94,0x94,
0x80,0x88,0x9e,0xa8,0x9c,0x8a,0xbc,0x88,
0x80,0xb0,0xb2,0x84,0x88,0x90,0xa6,0x86,
0x80,0x90,0xa8,0xa8,0x90,0xaa,0xa4,0x9a,
0x80,0x88,0x88,0x88,0x80,0x80,0x80,0x80,
0x80,0x88,0x90,0xa0,0xa0,0xa0,0x90,0x88,
0x80,0x88,0x84,0x82,0x82,0x82,0x84,0x88,
0x80,0x88,0xaa,0x9c,0x88,0x9c,0xaa,0x88,
0x80,0x80,0x88,0x88,0xbe,0x88,0x88,0x80,
0x80,0x80,0x80,0x80,0x80,0x88,0x88,0x90,
0x80,0x80,0x80,0x80,0xbe,0x80,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x88,
0x80,0x80,0x82,0x84,0x88,0x90,0xa0,0x80,
0x80,0x9c,0xa2,0xa6,0xaa,0xb2,0xa2,0x9c,
0x80,0x88,0x98,0x88,0x88,0x88,0x88,0x9c,
0x80,0x9c,0xa2,0x82,0x8c,0x90,0xa0,0xbe,
0x80,0xbe,0x82,0x84,0x8c,0x82,0xa2,0x9c,
0x80,0x84,0x8c,0x94,0xa4,0xbe,0x84,0x84,
0x80,0xbe,0xa0,0xbc,0x82,0x82,0xa2,0x9c,
0x80,0x8e,0x90,0xa0,0xbc,0xa2,0xa2,0x9c,
0x80,0xbe,0x82,0x84,0x88,0x90,0x90,0x90,
0x80,0x9c,0xa2,0xa2,0x9c,0xa2,0xa2,0x9c,
0x80,0x9c,0xa2,0xa2,0x9e,0x82,0x84,0xb8,
0x80,0x80,0x80,0x88,0x80,0x88,0x80,0x80,
0x80,0x80,0x80,0x88,0x80,0x88,0x88,0x90,
0x80,0x84,0x88,0x90,0xa0,0x90,0x88,0x84,
0x80,0x80,0x80,0xbe,0x80,0xbe,0x80,0x80,
0x80,0x90,0x88,0x84,0x82,0x84,0x88,0x90,
0x80,0x9c,0xa2,0x84,0x88,0x88,0x80,0x88,
0x00,0x1c,0x22,0x2a,0x2e,0x2c,0x20,0x1e,
0x00,0x08,0x14,0x22,0x22,0x3e,0x22,0x22,
0x00,0x3c,0x22,0x22,0x3c,0x22,0x22,0x3c,
0x00,0x1c,0x22,0x20,0x20,0x20,0x22,0x1c,
0x00,0x3c,0x22,0x22,0x22,0x22,0x22,0x3c,
0x00,0x3e,0x20,0x20,0x3c,0x20,0x20,0x3e,
0x00,0x3e,0x20,0x20,0x3c,0x20,0x20,0x20,
0x00,0x1e,0x20,0x20,0x20,0x26,0x22,0x1e,
0x00,0x22,0x22,0x22,0x3e,0x22,0x22,0x22,
0x00,0x1c,0x08,0x08,0x08,0x08,0x08,0x1c,
0x00,0x02,0x02,0x02,0x02,0x02,0x22,0x1c,
0x00,0x22,0x24,0x28,0x30,0x28,0x24,0x22,
0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x3e,
0x00,0x22,0x36,0x2a,0x2a,0x22,0x22,0x22,
0x00,0x22,0x22,0x32,0x2a,0x26,0x22,0x22,
0x00,0x1c,0x22,0x22,0x22,0x22,0x22,0x1c,
0x00,0x3c,0x22,0x22,0x3c,0x20,0x20,0x20,
0x00,0x1c,0x22,0x22,0x22,0x2a,0x24,0x1a,
0x00,0x3c,0x22,0x22,0x3c,0x28,0x24,0x22,
0x00,0x1c,0x22,0x20,0x1c,0x02,0x22,0x1c,
0x00,0x3e,0x08,0x08,0x08,0x08,0x08,0x08,
0x00,0x22,0x22,0x22,0x22,0x22,0x22,0x1c,
0x00,0x22,0x22,0x22,0x22,0x22,0x14,0x08,
0x00,0x22,0x22,0x22,0x2a,0x2a,0x36,0x22,
0x00,0x22,0x22,0x14,0x08,0x14,0x22,0x22,
0x00,0x22,0x22,0x14,0x08,0x08,0x08,0x08,
0x00,0x3e,0x02,0x04,0x08,0x10,0x20,0x3e,
0x00,0x3e,0x30,0x30,0x30,0x30,0x30,0x3e,
0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x00,
0x00,0x3e,0x06,0x06,0x06,0x06,0x06,0x3e,
0x00,0x00,0x00,0x08,0x14,0x22,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x08,0x08,0x08,0x08,0x08,0x00,0x08,
0x00,0x14,0x14,0x14,0x00,0x00,0x00,0x00,
0x00,0x14,0x14,0x3e,0x14,0x3e,0x14,0x14,
0x00,0x08,0x1e,0x28,0x1c,0x0a,0x3c,0x08,
0x00,0x30,0x32,0x04,0x08,0x10,0x26,0x06,
0x00,0x10,0x28,0x28,0x10,0x2a,0x24,0x1a,
0x00,0x08,0x08,0x08,0x00,0x00,0x00,0x00,
0x00,0x08,0x10,0x20,0x20,0x20,0x10,0x08,
0x00,0x08,0x04,0x02,0x02,0x02,0x04,0x08,
0x00,0x08,0x2a,0x1c,0x08,0x1c,0x2a,0x08,
0x00,0x00,0x08,0x08,0x3e,0x08,0x08,0x00,
0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x10,
0x00,0x00,0x00,0x00,0x3e,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,
0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x00,
0x00,0x1c,0x22,0x26,0x2a,0x32,0x22,0x1c,
0x00,0x08,0x18,0x08,0x08,0x08,0x08,0x1c,
0x00,0x1c,0x22,0x02,0x0c,0x10,0x20,0x3e,
0x00,0x3e,0x02,0x04,0x0c,0x02,0x22,0x1c,
0x00,0x04,0x0c,0x14,0x24,0x3e,0x04,0x04,
0x00,0x3e,0x20,0x3c,0x02,0x02,0x22,0x1c,
0x00,0x0e,0x10,0x20,0x3c,0x22,0x22,0x1c,
0x00,0x3e,0x02,0x04,0x08,0x10,0x10,0x10,
0x00,0x1c,0x22,0x22,0x1c,0x22,0x22,0x1c,
0x00,0x1c,0x22,0x22,0x1e,0x02,0x04,0x38,
0x00,0x00,0x00,0x08,0x00,0x08,0x00,0x00,
0x00,0x00,0x00,0x08,0x00,0x08,0x08,0x10,
0x00,0x04,0x08,0x10,0x20,0x10,0x08,0x04,
0x00,0x00,0x00,0x3e,0x00,0x3e,0x00,0x00,
0x00,0x10,0x08,0x04,0x02,0x04,0x08,0x10,
0x00,0x1c,0x22,0x04,0x08,0x08,0x00,0x08,
0x80,0x9c,0xa2,0xaa,0xae,0xac,0xa0,0x9e,
0x80,0x88,0x94,0xa2,0xa2,0xbe,0xa2,0xa2,
0x80,0xbc,0xa2,0xa2,0xbc,0xa2,0xa2,0xbc,
0x80,0x9c,0xa2,0xa0,0xa0,0xa0,0xa2,0x9c,
0x80,0xbc,0xa2,0xa2,0xa2,0xa2,0xa2,0xbc,
0x80,0xbe,0xa0,0xa0,0xbc,0xa0,0xa0,0xbe,
0x80,0xbe,0xa0,0xa0,0xbc,0xa0,0xa0,0xa0,
0x80,0x9e,0xa0,0xa0,0xa0,0xa6,0xa2,0x9e,
0x80,0xa2,0xa2,0xa2,0xbe,0xa2,0xa2,0xa2,
0x80,0x9c,0x88,0x88,0x88,0x88,0x88,0x9c,
0x80,0x82,0x82,0x82,0x82,0x82,0xa2,0x9c,
0x80,0xa2,0xa4,0xa8,0xb0,0xa8,0xa4,0xa2,
0x80,0xa0,0xa0,0xa0,0xa0,0xa0,0xa0,0xbe,
0x80,0xa2,0xb6,0xaa,0xaa,0xa2,0xa2,0xa2,
0x80,0xa2,0xa2,0xb2,0xaa,0xa6,0xa2,0xa2,
0x80,0x9c,0xa2,0xa2,0xa2,0xa2,0xa2,0x9c,
0x80,0xbc,0xa2,0xa2,0xbc,0xa0,0xa0,0xa0,
0x80,0x9c,0xa2,0xa2,0xa2,0xaa,0xa4,0x9a,
0x80,0xbc,0xa2,0xa2,0xbc,0xa8,0xa4,0xa2,
0x80,0x9c,0xa2,0xa0,0x9c,0x82,0xa2,0x9c,
0x80,0xbe,0x88,0x88,0x88,0x88,0x88,0x88,
0x80,0xa2,0xa2,0xa2,0xa2,0xa2,0xa2,0x9c,
0x80,0xa2,0xa2,0xa2,0xa2,0xa2,0x94,0x88,
0x80,0xa2,0xa2,0xa2,0xaa,0xaa,0xb6,0xa2,
0x80,0xa2,0xa2,0x94,0x88,0x94,0xa2,0xa2,
0x80,0xa2,0xa2,0x94,0x88,0x88,0x88,0x88,
0x80,0xbe,0x82,0x84,0x88,0x90,0xa0,0xbe,
0x80,0xbe,0xb0,0xb0,0xb0,0xb0,0xb0,0xbe,
0x80,0x80,0xa0,0x90,0x88,0x84,0x82,0x80,
0x80,0xbe,0x86,0x86,0x86,0x86,0x86,0xbe,
0x80,0x80,0x80,0x88,0x94,0xa2,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xbe,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,
0x80,0x88,0x88,0x88,0x88,0x88,0x80,0x88,
0x80,0x94,0x94,0x94,0x80,0x80,0x80,0x80,
0x80,0x94,0x94,0xbe,0x94,0xbe,0x94,0x94,
0x80,0x88,0x9e,0xa8,0x9c,0x8a,0xbc,0x88,
0x80,0xb0,0xb2,0x84,0x88,0x90,0xa6,0x86,
0x80,0x90,0xa8,0xa8,0x90,0xaa,0xa4,0x9a,
0x80,0x88,0x88,0x88,0x80,0x80,0x80,0x80,
0x80,0x88,0x90,0xa0,0xa0,0xa0,0x90,0x88,
0x80,0x88,0x84,0x82,0x82,0x82,0x84,0x88,
0x80,0x88,0xaa,0x9c,0x88,0x9c,0xaa,0x88,
0x80,0x80,0x88,0x88,0xbe,0x88,0x88,0x80,
0x80,0x80,0x80,0x80,0x80,0x88,0x88,0x90,
0x80,0x80,0x80,0x80,0xbe,0x80,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x88,
0x80,0x80,0x82,0x84,0x88,0x90,0xa0,0x80,
0x80,0x9c,0xa2,0xa6,0xaa,0xb2,0xa2,0x9c,
0x80,0x88,0x98,0x88,0x88,0x88,0x88,0x9c,
0x80,0x9c,0xa2,0x82,0x8c,0x90,0xa0,0xbe,
0x80,0xbe,0x82,0x84,0x8c,0x82,0xa2,0x9c,
0x80,0x84,0x8c,0x94,0xa4,0xbe,0x84,0x84,
0x80,0xbe,0xa0,0xbc,0x82,0x82,0xa2,0x9c,
0x80,0x8e,0x90,0xa0,0xbc,0xa2,0xa2,0x9c,
0x80,0xbe,0x82,0x84,0x88,0x90,0x90,0x90,
0x80,0x9c,0xa2,0xa2,0x9c,0xa2,0xa2,0x9c,
0x80,0x9c,0xa2,0xa2,0x9e,0x82,0x84,0xb8,
0x80,0x80,0x80,0x88,0x80,0x88,0x80,0x80,
0x80,0x80,0x80,0x88,0x80,0x88,0x88,0x90,
0x80,0x84,0x88,0x90,0xa0,0x90,0x88,0x84,
0x80,0x80,0x80,0xbe,0x80,0xbe,0x80,0x80,
0x80,0x90,0x88,0x84,0x82,0x84,0x88,0x90,
0x80,0x9c,0xa2,0x84,0x88,0x88,0x80,0x88
];
// public domain ROM (http://a2go.applearchives.com/roms/)
const APPLEIIGO_LZG = `TFpHAAAwAAAABYxwdy2NARUZHjRBUFBMRUlJR08gUk9NMS4wADQfNB80HzQfNB80HzQfNB80HDQGIADgGR97GR+uNB80Hxk/azQfNB8ZP2UZH4s0HzQfNB80HzQfNB80HzQfNB80HzQfNB80HTQPoCA0HCAgoBkOKAEQEAwFEw8GFCAODxQgARYBCQwBAgwFGRAoxs/SoM3P0sWgyc4eA83B1MnPzqDQzMXB08Wgw8zJw8ugHgigGQgo1MjFoMHQHhnJycfPoMzPNIHCxczP1x4cNAoZHvhMA+AgWPyiJ70A352ABMoQ9x4DMN+dAAUewx5OGQUDYB4DBh7DkB4ZBx4DTEDgNEEZP7s0HzQfNB80HzQfNB80HzQfNB80HzQfNB80HzQfNB80HzQfNB80HzQfNB80HjQYyc6w7snJkOrJzPDm0OjqNAtISikDCQSFKWgpGJACaX+FKAoKBSiFKGAZHnQApSUgwftlIBkdSzQVpSJIICT8pSiFKqUphSukIYhoaQHFI7ANHk6xKJEqiBD5MOGgACCe/LCGpCSpoJEoyMQhkPkZHsc0G6QksShIKT8JQJEoaGw4GRMbIAz9IKX7NKHJm/DzGRyNNAYgjv2lMyDt/aIBivDzyiA1/cmV0AKxKMngkAIp350AAsmN0LIgnPypjdBbpD2mPB4nIED5oACprUzt/RlfdjQfNB80HzQfNB80HzQfNB80HzQfNB80HjQFqQCFHKXmhRugAIQapRyRGiB+9MjQ9uYbpRspH9DuYIXihuCE4UgpwIUmSkoFJoUmaIUnCgoKJic0QmYmpScpHwXmhSeKwADwBaAjaQTI6Qew+4Tlqr259IUwmEql5IUcsBUcACM0BArJwBAGpRxJf4UcGf7aNB80HzQfNB80HzQfNBw0CkoIIEf4KKkPkAJp4IUusSZFMCUuUSaRJmAgAPjELLARyCAO+JD2aQFIHghoxS2Q9WCgL9ACoCeELaAnqQCFMCAo+IgQ9mAVBQR+JxUGBH4mCgoZgjRgpTAYaQMpD4UwCjQBBTCFMBln35AESjQBKQ8Za/CoSpAJarAQyaLwDCmHSqq9YvkgefjQBKCAqQCqvab5hS4pA4UvmCmPqpigA+CK8AtKkAhKSgkgiND6yIjQ8hmfOzQfNB80FNgghP4gL/sgk/4gif6tWMCtWsCtXcCtX8Ct/88sEMDYIDr/IGD7qQCFAKnGhQFsGR5vNB0VAxNs3dvHzxkKDa1wwKAA6uq9ZMAQBMjQ+IhgqQCFSK1WwK1UwK1RwKkA8AutUMCtU8AgNvipFIUiHhYgqSiFIakYhSOpF4UlTCL8IFj8oAm5CPuZDgSI0PdgrfMDSaWN9ANgyY3QGKwAwBATwJPQDywQwB5E+8CD8AMeBEz9+xUdB/gVEAf4yYfQEqlAIKj8oMCpDDTBrTDAiND1YKQkkSjmJKUkxSGwZmDJoLDvqBDsyY3wWsmKNGGI0MnGJBDopSGFJMYkpSLFJbALxiUVHAf4AEggJPwgnvygAGhpAMUjkPCwyqUihSWgAIQk8OSpAIUk5h4+HhC2xiUVHQf4FQYH+DhI6QHQ/Gg0gfZg5kLQAuZDpTzFPqU95T/mPB4GPRl99BUcB/jmTtAC5k8sAMAQ9ZEorQDALBDAYBUKB/j+YKUySKn/hTK9AAIg7f1oHoHJiPAdyZjwCuD4kAMgOv/o0BOp3B4VFQoH+P4VHgf4NBsASBmiWCDl/WgpDwmwybqQAmkGbDYAyaCQAiUyhDVIIHj7aKQ1GTEvQBkKBRkLGLE8kUIgtPyQ9xm+YTQBoD/QAqD/hDIZYlI+ojigG9AIHoI2oPClPikP8AYJwKAA8AKp/ZQAlQFg6upMFR8eQzQHqYdM7f2lSEilRaZGpEcZbhYZ34Q0GzQB9QP7A2L6Yvo=`;
///
/// Disk II
///
const NUM_DRIVES = 2;
const NUM_TRACKS = 35;
const TRACK_SIZE = 0x1880;
const SECTOR_SIZE = 383;
const DISKII_PROM = [
0xA2,0x20,0xA0,0x00,0xA2,0x03,0x86,0x3C,0x8A,0x0A,0x24,0x3C,0xF0,0x10,0x05,0x3C
,0x49,0xFF,0x29,0x7E,0xB0,0x08,0x4A,0xD0,0xFB,0x98,0x9D,0x56,0x03,0xC8,0xE8,0x10
,0xE5,0x20,0x58,0xFF,0xBA,0xBD,0x00,0x01,0x0A,0x0A,0x0A,0x0A,0x85,0x2B,0xAA,0xBD
,0x8E,0xC0,0xBD,0x8C,0xC0,0xBD,0x8A,0xC0,0xBD,0x89,0xC0,0xA0,0x50,0xBD,0x80,0xC0
,0x98,0x29,0x03,0x0A,0x05,0x2B,0xAA,0xBD,0x81,0xC0,0xA9,0x56,
/*0x20,0xA8,0xFC,*/0xa9,0x00,0xea,0x88
,0x10,0xEB,0x85,0x26,0x85,0x3D,0x85,0x41,0xA9,0x08,0x85,0x27,0x18,0x08,0xBD,0x8C
,0xC0,0x10,0xFB,0x49,0xD5,0xD0,0xF7,0xBD,0x8C,0xC0,0x10,0xFB,0xC9,0xAA,0xD0,0xF3
,0xEA,0xBD,0x8C,0xC0,0x10,0xFB,0xC9,0x96,0xF0,0x09,0x28,0x90,0xDF,0x49,0xAD,0xF0
,0x25,0xD0,0xD9,0xA0,0x03,0x85,0x40,0xBD,0x8C,0xC0,0x10,0xFB,0x2A,0x85,0x3C,0xBD
,0x8C,0xC0,0x10,0xFB,0x25,0x3C,0x88,0xD0,0xEC,0x28,0xC5,0x3D,0xD0,0xBE,0xA5,0x40
,0xC5,0x41,0xD0,0xB8,0xB0,0xB7,0xA0,0x56,0x84,0x3C,0xBC,0x8C,0xC0,0x10,0xFB,0x59
,0xD6,0x02,0xA4,0x3C,0x88,0x99,0x00,0x03,0xD0,0xEE,0x84,0x3C,0xBC,0x8C,0xC0,0x10
,0xFB,0x59,0xD6,0x02,0xA4,0x3C,0x91,0x26,0xC8,0xD0,0xEF,0xBC,0x8C,0xC0,0x10,0xFB
,0x59,0xD6,0x02,0xD0,0x87,0xA0,0x00,0xA2,0x56,0xCA,0x30,0xFB,0xB1,0x26,0x5E,0x00
,0x03,0x2A,0x5E,0x00,0x03,0x2A,0x91,0x26,0xC8,0xD0,0xEE,0xE6,0x27,0xE6,0x3D,0xA5
,0x3D,0xCD,0x00,0x08,0xA6,0x2B,0x90,0xDB,0x4C,0x01,0x08,0x00,0x00,0x00,0x00,0x00
];
class DiskIIState {
data : Uint8Array[];
track : number = 0;
read_mode : boolean = true;
write_protect : boolean = false;
motor : boolean = false;
track_index : number = 0;
}
class DiskII extends DiskIIState implements SlotDevice, SavesState<DiskIIState> {
emu : AppleII;
track_data : Uint8Array;
constructor(emu : AppleII, image : Uint8Array) {
super();
this.emu = emu;
this.data = new Array(NUM_TRACKS);
for (var i=0; i<NUM_TRACKS; i++) {
var ofs = i*16*256;
this.data[i] = nibblizeTrack(254, i, image.slice(ofs, ofs+16*256));
}
}
saveState() : DiskIIState {
var s = {
data: new Array(NUM_TRACKS),
track: this.track,
read_mode: this.read_mode,
write_protect: this.write_protect,
motor: this.motor,
track_index: this.track_index
};
for (var i=0; i<NUM_TRACKS; i++)
s.data[i] = this.data[i].slice(0);
return s;
}
loadState(s: DiskIIState) {
for (var i=0; i<NUM_TRACKS; i++)
this.data[i].set(s.data[i]);
this.track = s.track;
this.read_mode = s.read_mode;
this.write_protect = s.write_protect;
this.motor = s.motor;
this.track_index = s.track_index;
if ((this.track & 1) == 0)
this.track_data = this.data[this.track>>1];
else
this.track_data = null;
}
toLongString() {
return "Track: " + (this.track / 2) +
"\nOffset: " + (this.track_index) +
"\nMode: " + (this.read_mode ? "READ" : "WRITE") +
"\nMotor: " + this.motor +
"\nData: " + (this.track_data ? hex(this.track_data[this.track_index]) : '-') +
"\n";
}
read_latch() : number {
this.track_index = (this.track_index + 1) % TRACK_SIZE;
if (this.track_data) {
return (this.track_data[this.track_index] & 0xff);
} else
return this.emu.noise() | 0x80;
}
write_latch(value: number) {
this.track_index = (this.track_index + 1) % TRACK_SIZE;
if (this.track_data != null)
this.track_data[this.track_index] = value;
}
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); }
doIO(address, value) : number
{
switch (address & 0x0f)
{
/*
* Turn motor phases 0 to 3 on. Turning on the previous phase + 1
* increments the track position, turning on the previous phase - 1
* decrements the track position. In this scheme phase 0 and 3 are
* considered to be adjacent. The previous phase number can be
* computed as the track number % 4.
*/
case 0x1:
case 0x3:
case 0x5:
case 0x7:
var phase, lastphase, new_track;
new_track = this.track;
phase = (address >> 1) & 3;
// if new phase is even and current phase is odd
if (phase == ((new_track - 1) & 3))
{
if (new_track > 0)
new_track--;
} else
if (phase == ((new_track + 1) & 3))
{
if (new_track < NUM_TRACKS*2-1)
new_track++;
}
if ((new_track & 1) == 0)
{
this.track_data = this.data[new_track>>1];
console.log('track', new_track/2);
} else
this.track_data = null;
this.track = new_track;
break;
/*
* Turn drive motor off.
*/
case 0x8:
this.motor = false;
break;
/*
* Turn drive motor on.
*/
case 0x9:
this.motor = true;
break;
/*
* Select drive 1.
*/
case 0xa:
//drive = 0;
break;
/*
* Select drive 2.
*/
case 0xb:
//drive = 1;
break;
/*
* Select write mode.
*/
case 0xf:
this.read_mode = false;
/*
* Read a disk byte if read mode is active.
*/
case 0xC:
if (this.read_mode)
return this.read_latch();
break;
/*
* Select read mode and read the write protect status.
*/
case 0xE:
this.read_mode = true;
/*
* Write a disk byte if write mode is active and the disk is not
* write protected.
*/
case 0xD:
if (value >= 0 && !this.read_mode && !this.write_protect)
this.write_latch(value);
/*
* Read the write protect status only.
*/
return this.write_protect ? 0x80 : 0x00;
}
return this.emu.noise();
}
}
/* --------------- TRACK CONVERSION ROUTINES ---------------------- */
/*
* Normal byte (lower six bits only) -> disk byte translation table.
*/
const byte_translation = [
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
];
/*
* Sector skewing table.
*/
const skewing_table = [
0,7,14,6,13,5,12,4,11,3,10,2,9,1,8,15
];
/*
* Encode a 256-byte sector as SECTOR_SIZE disk bytes as follows:
*
* 14 sync bytes
* 3 address header bytes
* 8 address block bytes
* 3 address trailer bytes
* 6 sync bytes
* 3 data header bytes
* 343 data block bytes
* 3 data trailer bytes
*/
function nibblizeSector(vol, trk, sector, inn, in_ofs, out, i)
{
var loop, checksum, prev_value, value;
var sector_buffer = new Uint8Array(258);
value = 0;
/*
* Step 1: write 6 sync bytes (0xff's). Normally these would be
* written as 10-bit bytes with two extra zero bits, but for the
* purpose of emulation normal 8-bit bytes will do, since the
* emulated drive will always be in sync.
*/
for (loop = 0; loop < 14; loop++)
out[i++] = 0xff;
/*
* Step 2: write the 3-byte address header (0xd5 0xaa 0x96).
*/
out[i++] = 0xd5;
out[i++] = 0xaa;
out[i++] = 0x96;
/*
* Step 3: write the address block. Use 4-and-4 encoding to convert
* the volume, track and sector and checksum into 2 disk bytes each.
* The checksum is a simple exclusive OR of the first three values.
*/
out[i++] = ((vol >> 1) | 0xaa);
out[i++] = (vol | 0xaa);
checksum = vol;
out[i++] = ((trk >> 1) | 0xaa);
out[i++] = (trk | 0xaa);
checksum ^= trk;
out[i++] = ((sector >> 1) | 0xaa);
out[i++] = (sector | 0xaa);
checksum ^= sector;
out[i++] = ((checksum >> 1) | 0xaa);
out[i++] = (checksum | 0xaa);
/*
* Step 4: write the 3-byte address trailer (0xde 0xaa 0xeb).
*/
out[i++] = (0xde);
out[i++] = (0xaa);
out[i++] = (0xeb);
/*
* Step 5: write another 6 sync bytes.
*/
for (loop = 0; loop < 6; loop++)
out[i++] = (0xff);
/*
* Step 6: write the 3-byte data header.
*/
out[i++] = (0xd5);
out[i++] = (0xaa);
out[i++] = (0xad);
/*
* Step 7: read the next 256-byte sector from the old disk image file,
* and add two zero bytes to bring the number of bytes up to a multiple
* of 3.
*/
for (loop = 0; loop < 256; loop++)
sector_buffer[loop] = inn[loop + in_ofs] & 0xff;
sector_buffer[256] = 0;
sector_buffer[257] = 0;
/*
* Step 8: write the first 86 disk bytes of the data block, which
* encodes the bottom two bits of each sector byte into six-bit
* values as follows:
*
* disk byte n, bit 0 = sector byte n, bit 1
* disk byte n, bit 1 = sector byte n, bit 0
* disk byte n, bit 2 = sector byte n + 86, bit 1
* disk byte n, bit 3 = sector byte n + 86, bit 0
* disk byte n, bit 4 = sector byte n + 172, bit 1
* disk byte n, bit 5 = sector byte n + 172, bit 0
*
* The scheme allows each pair of bits to be shifted to the right out
* of the disk byte, then shifted to the left into the sector byte.
*
* Before the 6-bit value is translated to a disk byte, it is exclusive
* ORed with the previous 6-bit value, hence the values written are
* really a running checksum.
*/
prev_value = 0;
for (loop = 0; loop < 86; loop++)
{
value = (sector_buffer[loop] & 0x01) << 1;
value |= (sector_buffer[loop] & 0x02) >> 1;
value |= (sector_buffer[loop + 86] & 0x01) << 3;
value |= (sector_buffer[loop + 86] & 0x02) << 1;
value |= (sector_buffer[loop + 172] & 0x01) << 5;
value |= (sector_buffer[loop + 172] & 0x02) << 3;
out[i++] = (byte_translation[value ^ prev_value]);
prev_value = value;
}
/*
* Step 9: write the last 256 disk bytes of the data block, which
* encodes the top six bits of each sector byte. Again, each value
* is exclusive ORed with the previous value to create a running
* checksum (the first value is exclusive ORed with the last value of
* the previous step).
*/
for (loop = 0; loop < 256; loop++)
{
value = (sector_buffer[loop] >> 2);
out[i++] = (byte_translation[value ^ prev_value]);
prev_value = value;
}
/*
* Step 10: write the last value as the checksum.
*/
out[i++] = (byte_translation[value]);
/*
* Step 11: write the 3-byte data trailer.
*/
out[i++] = (0xde);
out[i++] = (0xaa);
out[i++] = (0xeb);
}
function nibblizeTrack(vol, trk, inn)
{
var out = new Uint8Array(TRACK_SIZE);
var out_pos = 0;
for (var sector = 0; sector < 16; sector++) {
nibblizeSector(vol, trk, sector,
inn, skewing_table[sector] << 8,
out, out_pos);
out_pos += SECTOR_SIZE;
}
while (out_pos < TRACK_SIZE)
out[out_pos++] = (0xff);
return out;
}