8bitworkshop/src/machine/apple2.ts

1473 lines
52 KiB
TypeScript

import { MOS6502, MOS6502State } from "../common/cpu/MOS6502";
import { Bus, BasicScanlineMachine, SavesState, AcceptsBIOS } from "../common/devices";
import { KeyFlags } from "../common/emu"; // TODO
import { hex, lzgmini, stringToByteArray, RGBA, printFlags } from "../common/util";
// 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;
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 implements AcceptsBIOS {
// approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/
cpuFrequency = 1022727;
sampleRate = this.cpuFrequency;
cpuCyclesPerLine = 65;
cpuCyclesPerFrame = this.cpuCyclesPerLine * 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;
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) => {
var pc = this.cpu.getPC();
if (pc >= 0xC600 && pc < 0xC700) {
// We're reading code to EXECUTE.
// Load the built program directly into memory, and "read"
// a JMP directly to it.
//console.log(`fakeDrive (EXEC): ${a.toString(16)}`);
switch (a) {
// JMP VM_BASE
case 0:
// SHOULD load program into RAM here, but have to do it
// below instead.
return 0;
case 1: return VM_BASE&0xff;
case 2: return (VM_BASE>>8)&0xff;
default: return 0;
}
}
else {
// We're reading code, but not executing it.
// This is probably the Monitor routine to identify whether
// this slot is a Disk ][ drive, so... give it what it wants.
//console.log(`fakeDrive (NOEX): ${a.toString(16)}`);
switch (a) {
case 0:
// Actually, if we get here, we probably ARE being
// executed. For some reason, the instruction at $C600
// gets read for execution, BEFORE the PC gets set to
// the correct location. So we handle loading the program
// into RAM and returning the JMP here, instead of above
// where it would otherwise belong.
if (this.rom) {
console.log(`Loading program into Apple ][ RAM at \$${PGM_BASE.toString(16)}`);
this.ram.set(this.rom.slice(HDR_SIZE), PGM_BASE);
}
return 0x4c; // JMP
case 1: return 0x20;
case 3: return 0x00;
case 5: return 0x03;
case 7: return 0x3c;
default: return 0;
}
}
},
readConst: (a) => {
return 0;
},
read: (a) => { return this.floatbus(); },
write: (a,v) => { }
};
constructor() {
super();
this.loadBIOS(new lzgmini().decode(stringToByteArray(atob(APPLEIIGO_LZG))));
this.connectCPUMemoryBus(this);
// This line is inappropriate for real ROMs, but was there for
// the APPLE][GO ROM, so keeping it only in the constructor, for
// that special case (in case it really is important for this
// address to be an RTS).
this.bios[0xD39A - (0x10000 - this.bios.length)] = 0x60; // $d39a = RTS
}
saveState() : AppleIIState {
// TODO: automagic
return {
c: this.cpu.saveState(),
ram: this.ram.slice(),
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.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;
}
loadBIOS(data, title?) {
if (data.length != 0x3000) {
console.log(`apple2 loadBIOS !!!WARNING!!!: BIOS wants length 0x3000, but BIOS '${title}' has length 0x${data.length.toString(16)}`);
console.log("will load BIOS to end of memory anyway...");
}
this.bios = Uint8Array.from(data);
this.ram.set(this.bios, 0x10000 - this.bios.length);
this.ram[0xbf00] = 0x4c; // fake DOS detect for C
this.ram[0xbf6f] = 0x01; // fake DOS detect for C
}
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.auxRAMselected = false;
this.auxRAMbank = 1;
this.writeinhibit = true;
this.ram.fill(0, 0x300, 0x400); // Clear soft-reset vector
// (force hard reset)
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;
}
}
readConst(address: number): number {
if (address < 0xc000) {
return this.ram[address];
} else if (address >= 0xd000) {
if (!this.auxRAMselected)
return this.bios[address - (0x10000 - this.bios.length)];
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) {
this.probe.logIORead(address, 0); // TODO: value
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.floatbus() & 0x7f;
// joystick
case 4:
case 5:
return this.floatbus() | 0x80;
default:
return this.floatbus();
}
case 7:
// joy reset
if (address == 0xc070)
return this.floatbus() | 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.floatbus();
}
write(address:number, val:number) : void {
address &= 0xffff;
val &= 0xff;
if (address < 0xc000) {
this.ram[address] = val;
this.grdirty[address>>7] = 1;
this.probe.logIOWrite(address, val);
} 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;
}
}
// http://www.deater.net/weave/vmwprod/megademo/vapor_lock.html
// https://retrocomputing.stackexchange.com/questions/14012/what-is-dram-refresh-and-why-is-the-weird-apple-ii-video-memory-layout-affected
// http://www.apple-iigs.info/doc/fichiers/TheappleIIcircuitdescription1.pdf
// http://rich12345.tripod.com/aiivideo/softalk.html
// https://github.com/MiSTer-devel/Apple-II_MiSTer/blob/master/rtl/timing_generator.vhd
floatbus() : number {
var fcyc = this.frameCycles;
var yline = Math.floor(fcyc / 65);
var xcyc = Math.floor(fcyc % 65);
var addr = this.ap2disp.getAddressForScanline(yline);
return this.readConst(addr + xcyc);
}
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 {
//console.log(`setKeyInput: ${key} ${code} ${flags}`);
if (flags & KeyFlags.KeyDown) {
code = 0;
switch (key) {
case 16: case 17: case 18:
break; // ignore shift/ctrl/etc
case 8:
code=8; // left
if (flags & KeyFlags.Shift) {
// (possibly) soft reset
this.cpu.reset();
return;
}
break;
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
case 48: if (flags & KeyFlags.Shift) code = 0x29; break; // )
case 49: if (flags & KeyFlags.Shift) code = 0x21; break; // !
case 50: if (flags & KeyFlags.Shift) code = 0x40; break; // @
case 51: if (flags & KeyFlags.Shift) code = 0x23; break; // #
case 52: if (flags & KeyFlags.Shift) code = 0x24; break; // $
case 53: if (flags & KeyFlags.Shift) code = 0x25; break; // %
case 54: if (flags & KeyFlags.Shift) code = 0x5e; break; // ^
case 55: if (flags & KeyFlags.Shift) code = 0x26; break; // &
case 56: if (flags & KeyFlags.Shift) code = 0x2a; break; // *
case 57: if (flags & KeyFlags.Shift) code = 0x28; break; // (
case 61: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // +
case 173: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _
case 59: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break;
case 188: code = (flags & KeyFlags.Shift) ? 0x3c : 0x2c; break;
case 190: code = (flags & KeyFlags.Shift) ? 0x3e : 0x2e; break;
case 191: code = (flags & KeyFlags.Shift) ? 0x3f : 0x2f; break;
case 222: code = (flags & KeyFlags.Shift) ? 0x22 : 0x27; break;
default:
code = key;
// convert to uppercase for Apple ][
if (code >= 0x61 && code <= 0x7a) code -= 32;
// convert to control codes if Ctrl pressed
if (code >= 65 && code < 65+26) {
if (flags & KeyFlags.Ctrl) code -= 64; // ctrl
}
}
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.floatbus();
}
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,
// just for floating bus, y >= 192
0x0078, 0x0478, 0x0878, 0x0c78, 0x1078, 0x1478, 0x1878, 0x1c78,
0x00f8, 0x04f8, 0x08f8, 0x0cf8, 0x10f8, 0x14f8, 0x18f8, 0x1cf8,
0x0178, 0x0578, 0x0978, 0x0d78, 0x1178, 0x1578, 0x1978, 0x1d78,
0x01f8, 0x05f8, 0x09f8, 0x0df8, 0x11f8, 0x15f8, 0x19f8, 0x1df8,
0x0278, 0x0678, 0x0a78, 0x0e78, 0x1278, 0x1678, 0x1a78, 0x1e78,
0x02f8, 0x06f8, 0x0af8, 0x0ef8, 0x12f8, 0x16f8, 0x1af8, 0x1ef8,
0x0378, 0x0778, 0x0b78, 0x0f78, 0x1378, 0x1778, 0x1b78, 0x1f78,
0x03f8, 0x07f8, 0x0bf8, 0x0ff8, 0x13f8, 0x17f8, 0x1bf8, 0x1ff8,
0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400,
];
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;
}
}
this.getAddressForScanline = function(y:number) : number {
var base = hires_lut[y];
if ((apple.grswitch & GR_HIRES) && (y < 160 || !(apple.grswitch & GR_MIXMODE)))
base = base | ((apple.grswitch & GR_PAGE1) ? 0x4000 : 0x2000);
else
base = (base & 0x3ff) | ((apple.grswitch & GR_PAGE1) ? 0x800 : 0x400);
return base;
}
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.floatbus() | 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.floatbus();
}
}
/* --------------- 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;
}