From 19b7e4edba6fa7e4afd0eefa6f103eb48037e2de Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 14:59:10 -0700 Subject: [PATCH 01/12] Reformat src/common/emu.ts --- src/common/emu.ts | 526 +++++++++++++++++++++++----------------------- 1 file changed, 263 insertions(+), 263 deletions(-) diff --git a/src/common/emu.ts b/src/common/emu.ts index d14ec15e..67a61e33 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -10,24 +10,24 @@ export var PLATFORMS = {}; var _random_state = 1; export function noise() { - let x = _random_state; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - return (_random_state = x) & 0xff; + let x = _random_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return (_random_state = x) & 0xff; } export function getNoiseSeed() { return _random_state; } -export function setNoiseSeed(x : number) { +export function setNoiseSeed(x: number) { _random_state = x; } -type KeyboardCallback = (which:number, charCode:number, flags:KeyFlags) => void; +type KeyboardCallback = (which: number, charCode: number, flags: KeyFlags) => void; -export function __createCanvas(doc:HTMLDocument, mainElement:HTMLElement, width:number, height:number) : HTMLCanvasElement { +export function __createCanvas(doc: HTMLDocument, mainElement: HTMLElement, width: number, height: number): HTMLCanvasElement { var canvas = doc.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -50,47 +50,47 @@ export enum KeyFlags { // TODO: don't use which/keyCode anymore? // TODO: let keycode = e.key ? e.key.charCodeAt(0) : e.keyCode; // TODO: let charCode = e.key ? e.key.charCodeAt(0) : e.charCode; -export function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) { +export function _setKeyboardEvents(canvas: HTMLElement, callback: KeyboardCallback) { canvas.onkeydown = (e) => { let flags = _metakeyflags(e); - callback(e.which, e.keyCode, KeyFlags.KeyDown|flags); + callback(e.which, e.keyCode, KeyFlags.KeyDown | flags); if (!flags) e.preventDefault(); // eat all keys that don't have a modifier }; canvas.onkeyup = (e) => { - callback(e.which, e.keyCode, KeyFlags.KeyUp|_metakeyflags(e)); + callback(e.which, e.keyCode, KeyFlags.KeyUp | _metakeyflags(e)); }; }; -type VideoCanvasOptions = {rotate?:number, overscan?:boolean, aspect?:number}; +type VideoCanvasOptions = { rotate?: number, overscan?: boolean, aspect?: number }; export class RasterVideo { - mainElement : HTMLElement; - width : number; - height : number; - options : VideoCanvasOptions; + mainElement: HTMLElement; + width: number; + height: number; + options: VideoCanvasOptions; - constructor(mainElement:HTMLElement, width:number, height:number, options?:VideoCanvasOptions) { + constructor(mainElement: HTMLElement, width: number, height: number, options?: VideoCanvasOptions) { this.mainElement = mainElement; this.width = width; this.height = height; this.options = options; } - - canvas : HTMLCanvasElement; - ctx : CanvasRenderingContext2D; - imageData : ImageData; - datau32 : Uint32Array; - vcanvas : JQuery; - + + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + imageData: ImageData; + datau32: Uint32Array; + vcanvas: JQuery; + paddle_x = 255; paddle_y = 255; - - setRotate(rotate:number) { + + setRotate(rotate: number) { var canvas = this.canvas; if (rotate) { // TODO: aspect ratio? - canvas.style.transform = "rotate("+rotate+"deg)"; + canvas.style.transform = "rotate(" + rotate + "deg)"; if (canvas.width < canvas.height) canvas.style.paddingLeft = canvas.style.paddingRight = "10%"; } else { @@ -107,11 +107,11 @@ export class RasterVideo { this.setRotate(this.options.rotate); } if (this.options && this.options.overscan) { - this.vcanvas.css('padding','0px'); + this.vcanvas.css('padding', '0px'); } if (this.options && this.options.aspect) { console.log(this.options); - this.vcanvas.css('aspect-ratio', this.options.aspect+""); + this.vcanvas.css('aspect-ratio', this.options.aspect + ""); } this.ctx = canvas.getContext('2d'); this.imageData = this.ctx.createImageData(this.width, this.height); @@ -126,22 +126,22 @@ export class RasterVideo { getContext() { return this.ctx; } - updateFrame(sx?:number, sy?:number, dx?:number, dy?:number, w?:number, h?:number) { + updateFrame(sx?: number, sy?: number, dx?: number, dy?: number, w?: number, h?: number) { if (w && h) this.ctx.putImageData(this.imageData, sx, sy, dx, dy, w, h); else this.ctx.putImageData(this.imageData, 0, 0); } - clearRect(dx:number, dy:number, w:number, h:number) { + clearRect(dx: number, dy: number, w: number, h: number) { var ctx = this.ctx; ctx.fillStyle = '#000000'; ctx.fillRect(dx, dy, w, h); } - setupMouseEvents(el? : HTMLCanvasElement) { + setupMouseEvents(el?: HTMLCanvasElement) { if (!el) el = this.canvas; - $(el).mousemove( (e) => { + $(el).mousemove((e) => { var pos = getMousePos(el, e); var new_x = Math.floor(pos.x * 255 / this.canvas.width); var new_y = Math.floor(pos.y * 255 / this.canvas.height); @@ -156,13 +156,13 @@ export class VectorVideo extends RasterVideo { persistenceAlpha = 0.5; jitter = 1.0; gamma = 0.8; - sx : number; - sy : number; - + sx: number; + sy: number; + create() { super.create(); - this.sx = this.width/1024.0; - this.sy = this.height/1024.0; + this.sx = this.width / 1024.0; + this.sy = this.height / 1024.0; } clear() { @@ -186,7 +186,7 @@ export class VectorVideo extends RasterVideo { '#ffffff' ]; - drawLine(x1:number, y1:number, x2:number, y2:number, intensity:number, color:number) { + drawLine(x1: number, y1: number, x2: number, y2: number, intensity: number, color: number) { var ctx = this.ctx; var sx = this.sx; var sy = this.sy; @@ -204,25 +204,25 @@ export class VectorVideo extends RasterVideo { x2 += jx; y1 += jy; y2 += jy; - ctx.moveTo(x1*sx, this.height-y1*sy); + ctx.moveTo(x1 * sx, this.height - y1 * sy); if (x1 == x2 && y1 == y2) - ctx.lineTo(x2*sx+1, this.height-y2*sy); + ctx.lineTo(x2 * sx + 1, this.height - y2 * sy); else - ctx.lineTo(x2*sx, this.height-y2*sy); + ctx.lineTo(x2 * sx, this.height - y2 * sy); ctx.strokeStyle = this.COLORS[color & 7]; ctx.stroke(); } } } -export function drawCrosshair(ctx:CanvasRenderingContext2D, x:number, y:number, width:number) { +export function drawCrosshair(ctx: CanvasRenderingContext2D, x: number, y: number, width: number) { if (!ctx?.setLineDash) return; // for unit testing ctx.fillStyle = 'rgba(0,0,0,0.25)'; - ctx.fillRect(x-2, 0, 5, 32767); - ctx.fillRect(0, y-2, 32767, 5); + ctx.fillRect(x - 2, 0, 5, 32767); + ctx.fillRect(0, y - 2, 32767, 5); ctx.lineWidth = width; ctx.strokeStyle = 'rgba(255,255,255,0.75)'; - ctx.setLineDash([width*2,width*2]); + ctx.setLineDash([width * 2, width * 2]); ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 32767); @@ -232,14 +232,14 @@ export function drawCrosshair(ctx:CanvasRenderingContext2D, x:number, y:number, } export class RAM { - mem : Uint8Array; - constructor(size:number) { + mem: Uint8Array; + constructor(size: number) { this.mem = new Uint8Array(new ArrayBuffer(size)); } } export class EmuHalt extends Error { - $loc : SourceLocation; + $loc: SourceLocation; squelchError = true; constructor(msg: string, loc?: SourceLocation) { super(msg); @@ -248,27 +248,27 @@ export class EmuHalt extends Error { } } -export var useRequestAnimationFrame : boolean = false; +export var useRequestAnimationFrame: boolean = false; export class AnimationTimer { - callback; - running : boolean = false; - pulsing : boolean = false; + callback; + running: boolean = false; + pulsing: boolean = false; nextts = 0; nframes; startts; // for FPS calc frameRate; intervalMsec; useReqAnimFrame = useRequestAnimationFrame && typeof window.requestAnimationFrame === 'function'; // need for unit test - - constructor(frequencyHz:number, callback:() => void) { + + constructor(frequencyHz: number, callback: () => void) { this.frameRate = frequencyHz; this.intervalMsec = 1000.0 / frequencyHz; this.callback = callback; } - scheduleFrame(msec:number) { + scheduleFrame(msec: number) { var fn = (timestamp) => { try { this.nextFrame(this.useReqAnimFrame ? timestamp : Date.now()); @@ -283,8 +283,8 @@ export class AnimationTimer { else setTimeout(fn, msec); } - - nextFrame(ts:number) { + + nextFrame(ts: number) { if (ts > this.nextts) { if (this.running) { this.callback(); @@ -292,7 +292,7 @@ export class AnimationTimer { if (this.nframes == 0) this.startts = ts; if (this.nframes++ == 300) { - console.log("Avg framerate: " + this.nframes*1000/(ts-this.startts) + " fps"); + console.log("Avg framerate: " + this.nframes * 1000 / (ts - this.startts) + " fps"); } } this.nextts += this.intervalMsec; @@ -328,19 +328,19 @@ export class AnimationTimer { // TODO: move to util? -export function dumpRAM(ram:ArrayLike, ramofs:number, ramlen:number) : string { +export function dumpRAM(ram: ArrayLike, ramofs: number, ramlen: number): string { var s = ""; var bpel = ram['BYTES_PER_ELEMENT'] || 1; var perline = Math.ceil(16 / bpel); var isFloat = ram instanceof Float32Array || ram instanceof Float64Array; // TODO: show scrollable RAM for other platforms - for (var ofs=0; ofs, ramofs:number, ramlen:number) : s } export interface KeyDef { - c:number, // key code - n:string, // name + c: number, // key code + n: string, // name // for gamepad - plyr?:number, - xaxis?:number, - yaxis?:number, - button?:number - }; + plyr?: number, + xaxis?: number, + yaxis?: number, + button?: number +}; export interface KeyMapEntry { - index:number; - mask:number; - def:KeyDef; + index: number; + mask: number; + def: KeyDef; } -type KeyCodeMap = Map; +type KeyCodeMap = Map; export const Keys = { - ANYKEY: {c: 0, n: "?"}, - // https://w3c.github.io/gamepad/#remapping - // gamepad and keyboard (player 0) - UP: {c: 38, n: "Up", plyr:0, button:12, yaxis:-1}, - DOWN: {c: 40, n: "Down", plyr:0, button:13, yaxis:1}, - LEFT: {c: 37, n: "Left", plyr:0, button:14, xaxis:-1}, - RIGHT: {c: 39, n: "Right", plyr:0, button:15, xaxis:1}, - A: {c: 32, n: "Space", plyr:0, button:0}, - B: {c: 16, n: "Shift", plyr:0, button:1}, - GP_A: {c: 88, n: "X", plyr:0, button:0}, - GP_B: {c: 90, n: "Z", plyr:0, button:1}, - GP_C: {c: 86, n: "V", plyr:0, button:2}, - GP_D: {c: 67, n: "C", plyr:0, button:3}, - SELECT: {c: 220, n: "\\", plyr:0, button:8}, - START: {c: 13, n: "Enter", plyr:0, button:9}, - OPTION: {c: 8, n: "Bcksp", plyr:0, button:10}, - // gamepad and keyboard (player 1) - P2_UP: {c: 87, n: "W", plyr:1, button:12, yaxis:-1}, - P2_DOWN: {c: 83, n: "S", plyr:1, button:13, yaxis:1}, - P2_LEFT: {c: 65, n: "A", plyr:1, button:14, xaxis:-1}, - P2_RIGHT: {c: 68, n: "D", plyr:1, button:15, xaxis:1}, - P2_A: {c: 84, n: "T", plyr:1, button:0}, - P2_B: {c: 82, n: "R", plyr:1, button:1}, - P2_GP_A: {c: 69, n: "E", plyr:1, button:0}, - P2_GP_B: {c: 82, n: "R", plyr:1, button:1}, - P2_GP_C: {c: 84, n: "T", plyr:1, button:2}, - P2_GP_D: {c: 89, n: "Y", plyr:1, button:3}, - P2_SELECT: {c: 70, n: "F", plyr:1, button:8}, - P2_START: {c: 71, n: "G", plyr:1, button:9}, - // keyboard only - VK_ESCAPE: {c: 27, n: "Esc"}, - VK_F1: {c: 112, n: "F1"}, - VK_F2: {c: 113, n: "F2"}, - VK_F3: {c: 114, n: "F3"}, - VK_F4: {c: 115, n: "F4"}, - VK_F5: {c: 116, n: "F5"}, - VK_F6: {c: 117, n: "F6"}, - VK_F7: {c: 118, n: "F7"}, - VK_F8: {c: 119, n: "F8"}, - VK_F9: {c: 120, n: "F9"}, - VK_F10: {c: 121, n: "F10"}, - VK_F11: {c: 122, n: "F11"}, - VK_F12: {c: 123, n: "F12"}, - VK_SCROLL_LOCK: {c: 145, n: "ScrLck"}, - VK_PAUSE: {c: 19, n: "Pause"}, - VK_QUOTE: {c: 222, n: "'"}, - VK_1: {c: 49, n: "1"}, - VK_2: {c: 50, n: "2"}, - VK_3: {c: 51, n: "3"}, - VK_4: {c: 52, n: "4"}, - VK_5: {c: 53, n: "5"}, - VK_6: {c: 54, n: "6"}, - VK_7: {c: 55, n: "7"}, - VK_8: {c: 56, n: "8"}, - VK_9: {c: 57, n: "9"}, - VK_0: {c: 48, n: "0"}, - VK_MINUS: {c: 189, n: "-"}, - VK_MINUS2: {c: 173, n: "-"}, - VK_EQUALS: {c: 187, n: "="}, - VK_EQUALS2: {c: 61, n: "="}, - VK_BACK_SPACE: {c: 8, n: "Bkspc"}, - VK_TAB: {c: 9, n: "Tab"}, - VK_Q: {c: 81, n: "Q"}, - VK_W: {c: 87, n: "W"}, - VK_E: {c: 69, n: "E"}, - VK_R: {c: 82, n: "R"}, - VK_T: {c: 84, n: "T"}, - VK_Y: {c: 89, n: "Y"}, - VK_U: {c: 85, n: "U"}, - VK_I: {c: 73, n: "I"}, - VK_O: {c: 79, n: "O"}, - VK_P: {c: 80, n: "P"}, - VK_ACUTE: {c: 219, n: "´"}, - VK_OPEN_BRACKET: {c: 219, n: "["}, - VK_CLOSE_BRACKET: {c: 221, n: "]"}, - VK_CAPS_LOCK: {c: 20, n: "CpsLck"}, - VK_A: {c: 65, n: "A"}, - VK_S: {c: 83, n: "S"}, - VK_D: {c: 68, n: "D"}, - VK_F: {c: 70, n: "F"}, - VK_G: {c: 71, n: "G"}, - VK_H: {c: 72, n: "H"}, - VK_J: {c: 74, n: "J"}, - VK_K: {c: 75, n: "K"}, - VK_L: {c: 76, n: "L"}, - VK_CEDILLA: {c: 186, n: "Ç"}, - VK_TILDE: {c: 222, n: "~"}, - VK_ENTER: {c: 13, n: "Enter"}, - VK_SHIFT: {c: 16, n: "Shift"}, - VK_BACK_SLASH: {c: 220, n: "\\"}, - VK_Z: {c: 90, n: "Z"}, - VK_X: {c: 88, n: "X"}, - VK_C: {c: 67, n: "C"}, - VK_V: {c: 86, n: "V"}, - VK_B: {c: 66, n: "B"}, - VK_N: {c: 78, n: "N"}, - VK_M: {c: 77, n: "M"}, - VK_COMMA: {c: 188, n: "] ="}, - VK_PERIOD: {c: 190, n: "."}, - VK_SEMICOLON: {c: 59, n: ";"}, - VK_SLASH: {c: 191, n: "/"}, - VK_CONTROL: {c: 17, n: "Ctrl"}, - VK_ALT: {c: 18, n: "Alt"}, - VK_COMMAND: {c: 224, n: "Cmd"}, - VK_SPACE: {c: 32, n: "Space"}, - VK_INSERT: {c: 45, n: "Ins"}, - VK_DELETE: {c: 46, n: "Del"}, - VK_HOME: {c: 36, n: "Home"}, - VK_END: {c: 35, n: "End"}, - VK_PAGE_UP: {c: 33, n: "PgUp"}, - VK_PAGE_DOWN: {c: 34, n: "PgDown"}, - VK_UP: {c: 38, n: "Up"}, - VK_DOWN: {c: 40, n: "Down"}, - VK_LEFT: {c: 37, n: "Left"}, - VK_RIGHT: {c: 39, n: "Right"}, - VK_NUM_LOCK: {c: 144, n: "Num"}, - VK_DIVIDE: {c: 111, n: "Num /"}, - VK_MULTIPLY: {c: 106, n: "Num *"}, - VK_SUBTRACT: {c: 109, n: "Num -"}, - VK_ADD: {c: 107, n: "Num +"}, - VK_DECIMAL: {c: 194, n: "Num ."}, - VK_NUMPAD0: {c: 96, n: "Num 0"}, - VK_NUMPAD1: {c: 97, n: "Num 1"}, - VK_NUMPAD2: {c: 98, n: "Num 2"}, - VK_NUMPAD3: {c: 99, n: "Num 3"}, - VK_NUMPAD4: {c: 100, n: "Num 4"}, - VK_NUMPAD5: {c: 101, n: "Num 5"}, - VK_NUMPAD6: {c: 102, n: "Num 6"}, - VK_NUMPAD7: {c: 103, n: "Num 7"}, - VK_NUMPAD8: {c: 104, n: "Num 8"}, - VK_NUMPAD9: {c: 105, n: "Num 9"}, - VK_NUMPAD_CENTER: {c: 12, n: "Num Cntr"} + ANYKEY: { c: 0, n: "?" }, + // https://w3c.github.io/gamepad/#remapping + // gamepad and keyboard (player 0) + UP: { c: 38, n: "Up", plyr: 0, button: 12, yaxis: -1 }, + DOWN: { c: 40, n: "Down", plyr: 0, button: 13, yaxis: 1 }, + LEFT: { c: 37, n: "Left", plyr: 0, button: 14, xaxis: -1 }, + RIGHT: { c: 39, n: "Right", plyr: 0, button: 15, xaxis: 1 }, + A: { c: 32, n: "Space", plyr: 0, button: 0 }, + B: { c: 16, n: "Shift", plyr: 0, button: 1 }, + GP_A: { c: 88, n: "X", plyr: 0, button: 0 }, + GP_B: { c: 90, n: "Z", plyr: 0, button: 1 }, + GP_C: { c: 86, n: "V", plyr: 0, button: 2 }, + GP_D: { c: 67, n: "C", plyr: 0, button: 3 }, + SELECT: { c: 220, n: "\\", plyr: 0, button: 8 }, + START: { c: 13, n: "Enter", plyr: 0, button: 9 }, + OPTION: { c: 8, n: "Bcksp", plyr: 0, button: 10 }, + // gamepad and keyboard (player 1) + P2_UP: { c: 87, n: "W", plyr: 1, button: 12, yaxis: -1 }, + P2_DOWN: { c: 83, n: "S", plyr: 1, button: 13, yaxis: 1 }, + P2_LEFT: { c: 65, n: "A", plyr: 1, button: 14, xaxis: -1 }, + P2_RIGHT: { c: 68, n: "D", plyr: 1, button: 15, xaxis: 1 }, + P2_A: { c: 84, n: "T", plyr: 1, button: 0 }, + P2_B: { c: 82, n: "R", plyr: 1, button: 1 }, + P2_GP_A: { c: 69, n: "E", plyr: 1, button: 0 }, + P2_GP_B: { c: 82, n: "R", plyr: 1, button: 1 }, + P2_GP_C: { c: 84, n: "T", plyr: 1, button: 2 }, + P2_GP_D: { c: 89, n: "Y", plyr: 1, button: 3 }, + P2_SELECT: { c: 70, n: "F", plyr: 1, button: 8 }, + P2_START: { c: 71, n: "G", plyr: 1, button: 9 }, + // keyboard only + VK_ESCAPE: { c: 27, n: "Esc" }, + VK_F1: { c: 112, n: "F1" }, + VK_F2: { c: 113, n: "F2" }, + VK_F3: { c: 114, n: "F3" }, + VK_F4: { c: 115, n: "F4" }, + VK_F5: { c: 116, n: "F5" }, + VK_F6: { c: 117, n: "F6" }, + VK_F7: { c: 118, n: "F7" }, + VK_F8: { c: 119, n: "F8" }, + VK_F9: { c: 120, n: "F9" }, + VK_F10: { c: 121, n: "F10" }, + VK_F11: { c: 122, n: "F11" }, + VK_F12: { c: 123, n: "F12" }, + VK_SCROLL_LOCK: { c: 145, n: "ScrLck" }, + VK_PAUSE: { c: 19, n: "Pause" }, + VK_QUOTE: { c: 222, n: "'" }, + VK_1: { c: 49, n: "1" }, + VK_2: { c: 50, n: "2" }, + VK_3: { c: 51, n: "3" }, + VK_4: { c: 52, n: "4" }, + VK_5: { c: 53, n: "5" }, + VK_6: { c: 54, n: "6" }, + VK_7: { c: 55, n: "7" }, + VK_8: { c: 56, n: "8" }, + VK_9: { c: 57, n: "9" }, + VK_0: { c: 48, n: "0" }, + VK_MINUS: { c: 189, n: "-" }, + VK_MINUS2: { c: 173, n: "-" }, + VK_EQUALS: { c: 187, n: "=" }, + VK_EQUALS2: { c: 61, n: "=" }, + VK_BACK_SPACE: { c: 8, n: "Bkspc" }, + VK_TAB: { c: 9, n: "Tab" }, + VK_Q: { c: 81, n: "Q" }, + VK_W: { c: 87, n: "W" }, + VK_E: { c: 69, n: "E" }, + VK_R: { c: 82, n: "R" }, + VK_T: { c: 84, n: "T" }, + VK_Y: { c: 89, n: "Y" }, + VK_U: { c: 85, n: "U" }, + VK_I: { c: 73, n: "I" }, + VK_O: { c: 79, n: "O" }, + VK_P: { c: 80, n: "P" }, + VK_ACUTE: { c: 219, n: "´" }, + VK_OPEN_BRACKET: { c: 219, n: "[" }, + VK_CLOSE_BRACKET: { c: 221, n: "]" }, + VK_CAPS_LOCK: { c: 20, n: "CpsLck" }, + VK_A: { c: 65, n: "A" }, + VK_S: { c: 83, n: "S" }, + VK_D: { c: 68, n: "D" }, + VK_F: { c: 70, n: "F" }, + VK_G: { c: 71, n: "G" }, + VK_H: { c: 72, n: "H" }, + VK_J: { c: 74, n: "J" }, + VK_K: { c: 75, n: "K" }, + VK_L: { c: 76, n: "L" }, + VK_CEDILLA: { c: 186, n: "Ç" }, + VK_TILDE: { c: 222, n: "~" }, + VK_ENTER: { c: 13, n: "Enter" }, + VK_SHIFT: { c: 16, n: "Shift" }, + VK_BACK_SLASH: { c: 220, n: "\\" }, + VK_Z: { c: 90, n: "Z" }, + VK_X: { c: 88, n: "X" }, + VK_C: { c: 67, n: "C" }, + VK_V: { c: 86, n: "V" }, + VK_B: { c: 66, n: "B" }, + VK_N: { c: 78, n: "N" }, + VK_M: { c: 77, n: "M" }, + VK_COMMA: { c: 188, n: "] =" }, + VK_PERIOD: { c: 190, n: "." }, + VK_SEMICOLON: { c: 59, n: ";" }, + VK_SLASH: { c: 191, n: "/" }, + VK_CONTROL: { c: 17, n: "Ctrl" }, + VK_ALT: { c: 18, n: "Alt" }, + VK_COMMAND: { c: 224, n: "Cmd" }, + VK_SPACE: { c: 32, n: "Space" }, + VK_INSERT: { c: 45, n: "Ins" }, + VK_DELETE: { c: 46, n: "Del" }, + VK_HOME: { c: 36, n: "Home" }, + VK_END: { c: 35, n: "End" }, + VK_PAGE_UP: { c: 33, n: "PgUp" }, + VK_PAGE_DOWN: { c: 34, n: "PgDown" }, + VK_UP: { c: 38, n: "Up" }, + VK_DOWN: { c: 40, n: "Down" }, + VK_LEFT: { c: 37, n: "Left" }, + VK_RIGHT: { c: 39, n: "Right" }, + VK_NUM_LOCK: { c: 144, n: "Num" }, + VK_DIVIDE: { c: 111, n: "Num /" }, + VK_MULTIPLY: { c: 106, n: "Num *" }, + VK_SUBTRACT: { c: 109, n: "Num -" }, + VK_ADD: { c: 107, n: "Num +" }, + VK_DECIMAL: { c: 194, n: "Num ." }, + VK_NUMPAD0: { c: 96, n: "Num 0" }, + VK_NUMPAD1: { c: 97, n: "Num 1" }, + VK_NUMPAD2: { c: 98, n: "Num 2" }, + VK_NUMPAD3: { c: 99, n: "Num 3" }, + VK_NUMPAD4: { c: 100, n: "Num 4" }, + VK_NUMPAD5: { c: 101, n: "Num 5" }, + VK_NUMPAD6: { c: 102, n: "Num 6" }, + VK_NUMPAD7: { c: 103, n: "Num 7" }, + VK_NUMPAD8: { c: 104, n: "Num 8" }, + VK_NUMPAD9: { c: 105, n: "Num 9" }, + VK_NUMPAD_CENTER: { c: 12, n: "Num Cntr" } }; function _metakeyflags(e) { - return (e.shiftKey?KeyFlags.Shift:0) | - (e.ctrlKey?KeyFlags.Ctrl:0) | - (e.altKey?KeyFlags.Alt:0) | - (e.metaKey?KeyFlags.Meta:0); + return (e.shiftKey ? KeyFlags.Shift : 0) | + (e.ctrlKey ? KeyFlags.Ctrl : 0) | + (e.altKey ? KeyFlags.Alt : 0) | + (e.metaKey ? KeyFlags.Meta : 0); } -type KeyMapFunction = (o:KeyMapEntry, key:number, code:number, flags:number) => void; +type KeyMapFunction = (o: KeyMapEntry, key: number, code: number, flags: number) => void; -export function newKeyboardHandler(switches:number[]|Uint8Array, map:KeyCodeMap, func?:KeyMapFunction, alwaysfunc?:boolean) { - return (key:number,code:number,flags:number) => { +export function newKeyboardHandler(switches: number[] | Uint8Array, map: KeyCodeMap, func?: KeyMapFunction, alwaysfunc?: boolean) { + return (key: number, code: number, flags: number) => { if (!map) { func(null, key, code, flags); return; } - var o : KeyMapEntry = map[key]; + var o: KeyMapEntry = map[key]; if (!o) o = map[0]; if (func && (o || alwaysfunc)) { func(o, key, code, flags); @@ -538,23 +538,23 @@ export function newKeyboardHandler(switches:number[]|Uint8Array, map:KeyCodeMap, }; } -export function setKeyboardFromMap(video:RasterVideo, switches:number[]|Uint8Array, map:KeyCodeMap, func?:KeyMapFunction, alwaysfunc?:boolean) { +export function setKeyboardFromMap(video: RasterVideo, switches: number[] | Uint8Array, map: KeyCodeMap, func?: KeyMapFunction, alwaysfunc?: boolean) { var handler = newKeyboardHandler(switches, map, func, alwaysfunc); video.setKeyboardEvents(handler); return new ControllerPoller(handler); } -export function makeKeycodeMap(table : [KeyDef,number,number][]) : KeyCodeMap { - var map = new Map(); - for (var i=0; i(); + for (var i = 0; i < table.length; i++) { var entry = table[i]; - var val : KeyMapEntry = {index:entry[1], mask:entry[2], def:entry[0]}; + var val: KeyMapEntry = { index: entry[1], mask: entry[2], def: entry[0] }; map[entry[0].c] = val; } return map; } -const DEFAULT_CONTROLLER_KEYS : KeyDef[] = [ +const DEFAULT_CONTROLLER_KEYS: KeyDef[] = [ Keys.UP, Keys.DOWN, Keys.LEFT, Keys.RIGHT, Keys.A, Keys.B, Keys.SELECT, Keys.START, Keys.P2_UP, Keys.P2_DOWN, Keys.P2_LEFT, Keys.P2_RIGHT, Keys.P2_A, Keys.P2_B, Keys.P2_SELECT, Keys.P2_START, ]; @@ -562,10 +562,10 @@ const DEFAULT_CONTROLLER_KEYS : KeyDef[] = [ export class ControllerPoller { active = false; handler; - state : Int32Array[]; - lastState : Int32Array[]; + state: Int32Array[]; + lastState: Int32Array[]; AXIS0 = 24; // first joystick axis index - constructor(handler:(key,code,flags) => void) { + constructor(handler: (key, code, flags) => void) { this.handler = handler; window.addEventListener("gamepadconnected", (event) => { console.log("Gamepad connected:", event); @@ -582,7 +582,7 @@ export class ControllerPoller { let numGamepads = navigator.getGamepads().length; this.state = new Array(numGamepads); this.lastState = new Array(numGamepads); - for (var i=0; i len) { throw Error("Data too long, " + data.length + " > " + len); } var r = new RAM(len); if (padstart) - r.mem.set(data, len-data.length); + r.mem.set(data, len - data.length); else r.mem.set(data); return r.mem; } -type AddressReadWriteFn = ((a:number) => number) | ((a:number,v:number) => void); +type AddressReadWriteFn = ((a: number) => number) | ((a: number, v: number) => void); type AddressDecoderEntry = [number, number, number, AddressReadWriteFn]; -type AddressDecoderOptions = {gmask?:number, defaultval?:number}; +type AddressDecoderOptions = { gmask?: number, defaultval?: number }; // TODO: better performance, check values -export function AddressDecoder(table : AddressDecoderEntry[], options?:AddressDecoderOptions) { +export function AddressDecoder(table: AddressDecoderEntry[], options?: AddressDecoderOptions) { var self = this; function makeFunction() { var s = ""; if (options && options.gmask) { s += "a&=" + options.gmask + ";"; } - for (var i=0; i number { +export function newAddressDecoder(table: AddressDecoderEntry[], options?: AddressDecoderOptions): (a: number, v?: number) => number { return new (AddressDecoder as any)(table, options); } // https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas -export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} { +export function getMousePos(canvas: HTMLCanvasElement, evt): { x: number, y: number } { var rect = canvas.getBoundingClientRect(), // abs. size of element - scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X - scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y + scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X + scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y return { x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have @@ -705,35 +705,35 @@ export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:numbe /// // TODO: https://stackoverflow.com/questions/10463518/converting-em-to-px-in-javascript-and-getting-default-font-size -export function getVisibleEditorLineHeight() : number{ +export function getVisibleEditorLineHeight(): number { return $("#booksMenuButton").first().height(); } export interface VirtualTextLine { - text : string; - clas? : string; + text: string; + clas?: string; } export class VirtualTextScroller { memorylist; - maindiv : HTMLElement; - getLineAt : (row:number) => VirtualTextLine; + maindiv: HTMLElement; + getLineAt: (row: number) => VirtualTextLine; - constructor(parent : HTMLElement) { + constructor(parent: HTMLElement) { var div = document.createElement('div'); div.setAttribute("class", "memdump"); parent.appendChild(div); this.maindiv = div; } - - create(workspace : HTMLElement, maxRowCount : number, fn : (row:number) => VirtualTextLine) { + + create(workspace: HTMLElement, maxRowCount: number, fn: (row: number) => VirtualTextLine) { this.getLineAt = fn; this.memorylist = new VirtualList({ w: $(workspace).width(), h: $(workspace).height(), itemHeight: getVisibleEditorLineHeight(), totalRows: maxRowCount, // TODO? - generatorFn: (row : number) => { + generatorFn: (row: number) => { var line = fn(row); var linediv = document.createElement("div"); linediv.appendChild(document.createTextNode(line.text)); @@ -747,7 +747,7 @@ export class VirtualTextScroller { // TODO: refactor with elsewhere refresh() { if (this.memorylist) { - $(this.maindiv).find('[data-index]').each( (i,e) => { + $(this.maindiv).find('[data-index]').each((i, e) => { var div = e; var row = parseInt(div.getAttribute('data-index')); var oldtext = div.innerText; @@ -783,5 +783,5 @@ export function gtia_ntsc_to_rgb(val: number) { var r = y + 0.956 * i + 0.621 * q; var g = y - 0.272 * i - 0.647 * q; var b = y - 1.107 * i + 1.704 * q; - return RGBA(clamp(0,255,r), clamp(0,255,g), clamp(0,255,b)); + return RGBA(clamp(0, 255, r), clamp(0, 255, g), clamp(0, 255, b)); } From 8ef91b935d475dfbcf81552b51875ac24e6d311f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 13:48:26 -0700 Subject: [PATCH 02/12] Improve canvas mouse position calculations Fix mouse cacluations to account for non-integer canvas scale, and rect posistion & size, and replace Math.floor() with Math.round(). Fixes - Unreachable max x/y value in some cases. - Disproportionate number of mouse pixels for min and max mouse positions. --- src/common/emu.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/common/emu.ts b/src/common/emu.ts index 67a61e33..0594e1d5 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -143,8 +143,8 @@ export class RasterVideo { if (!el) el = this.canvas; $(el).mousemove((e) => { var pos = getMousePos(el, e); - var new_x = Math.floor(pos.x * 255 / this.canvas.width); - var new_y = Math.floor(pos.y * 255 / this.canvas.height); + var new_x = Math.round(pos.x * 255 / this.canvas.width); + var new_y = Math.round(pos.y * 255 / this.canvas.height); this.paddle_x = clamp(0, 255, new_x); this.paddle_y = clamp(0, 255, new_y); }); @@ -692,13 +692,24 @@ export function newAddressDecoder(table: AddressDecoderEntry[], options?: Addres // https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas export function getMousePos(canvas: HTMLCanvasElement, evt): { x: number, y: number } { + // Improves reachability of min and max values at edges of canvas. Without delta, + // min and max values would be represented by 1/2 as many pixels as other values. + const delta = .5; + // Canvas HTML element has integer `width` and `height`, but getBoundingClientRect() pos & size + // are scaled to non-integer values. Mouse events return integer x/y values in the range + // [Math.floor(rect.left/top), Math.floor(rect.left/top) + Math.floor(rect.width/height)], + // which may not be the same as [Math.floor(rect.left/top), Math.floor(rect.right/bottom)]. var rect = canvas.getBoundingClientRect(), // abs. size of element - scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X - scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y + scaleX = canvas.width / Math.floor(rect.width - 2 * delta), + scaleY = canvas.height / Math.floor(rect.height - 2 * delta), + x = (evt.clientX - delta - Math.floor(rect.left)) * scaleX, // scale mouse coordinates after they have + y = (evt.clientY - delta - Math.floor(rect.top)) * scaleY; // been adjusted to be relative to element return { - x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have - y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element + // Clamp to [0, canvas.width/height] to prevent non-integer rect position + // and size, and potentially `delta`, from returning out of bounds values. + x: Math.max(0, Math.min(canvas.width, x)), + y: Math.max(0, Math.min(canvas.height, y)) } } From 4334485271d80ca9685e508eaac56dc527216503 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 14:51:48 -0700 Subject: [PATCH 03/12] Mouse positions relatative to canvas content Ignore canvas borders and padding. This has the nice benefit that it's much easier to land on min/max mouse x/y, especially when moving the mouse quickly. --- src/common/emu.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/common/emu.ts b/src/common/emu.ts index 0594e1d5..386008af 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -700,11 +700,22 @@ export function getMousePos(canvas: HTMLCanvasElement, evt): { x: number, y: num // [Math.floor(rect.left/top), Math.floor(rect.left/top) + Math.floor(rect.width/height)], // which may not be the same as [Math.floor(rect.left/top), Math.floor(rect.right/bottom)]. var rect = canvas.getBoundingClientRect(), // abs. size of element - scaleX = canvas.width / Math.floor(rect.width - 2 * delta), - scaleY = canvas.height / Math.floor(rect.height - 2 * delta), - x = (evt.clientX - delta - Math.floor(rect.left)) * scaleX, // scale mouse coordinates after they have - y = (evt.clientY - delta - Math.floor(rect.top)) * scaleY; // been adjusted to be relative to element - + // Ignore canvas borders and padding, so mouse position is relative to canvas content. + style = window.getComputedStyle(canvas), + left = parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft), + right = parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight), + top = parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop), + bottom = parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom), + width = rect.width - left - right, + height = rect.height - top - bottom, + + // Determine scale factors to map mouse coordinates to canvas coordinates. + scaleX = canvas.width / Math.floor(width - 2 * delta), + scaleY = canvas.height / Math.floor(height - 2 * delta), + + // Scale coordinates after they've been adjusted to be relative to canvas content. + x = (evt.clientX - delta - rect.left - left) * scaleX, + y = (evt.clientY - delta - rect.top - top) * scaleY; return { // Clamp to [0, canvas.width/height] to prevent non-integer rect position // and size, and potentially `delta`, from returning out of bounds values. From feece725ce65161e082b2a1a1d66ac289104805b Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 15:13:40 -0700 Subject: [PATCH 04/12] Paddles/joystick start centered in [0,255] range Values remain centered until mouse move events are received. --- src/common/emu.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/emu.ts b/src/common/emu.ts index 386008af..902f0d26 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -83,8 +83,9 @@ export class RasterVideo { datau32: Uint32Array; vcanvas: JQuery; - paddle_x = 255; - paddle_y = 255; + // Start paddles/joystick centered in [0,255] range. + paddle_x = 128; + paddle_y = 128; setRotate(rotate: number) { var canvas = this.canvas; From 4a0741003313fa0113d9f972790e16cedfdbcc25 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 14:58:56 -0700 Subject: [PATCH 05/12] Reformat src/machine/apple2.ts --- src/machine/apple2.ts | 2521 ++++++++++++++++++++--------------------- 1 file changed, 1237 insertions(+), 1284 deletions(-) diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index c8c96a95..e610e2a8 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -1,171 +1,170 @@ - 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, arrayCompare } from "../common/util"; interface AppleIIStateBase { - ram : Uint8Array; - soundstate : number; - auxRAMselected,writeinhibit : boolean; - auxRAMbank : number; + ram: Uint8Array; + soundstate: number; + auxRAMselected, writeinhibit: boolean; + auxRAMbank: number; } interface AppleIIControlsState { - inputs : Uint8Array; // unused? - kbdlatch : number; + inputs: Uint8Array; // unused? + kbdlatch: number; } interface AppleIIState extends AppleIIStateBase, AppleIIControlsState { - c : MOS6502State; - grswitch : number; - slots: SlotDevice[]; + c: MOS6502State; + grswitch: number; + slots: SlotDevice[]; } interface SlotDevice extends Bus { - readROM(address: number) : number; - readConst(address: number) : number; + 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 = 0x13000; // we'll never need one that big, but... - - // these are set later - LOAD_BASE = 0; - HDR_SIZE = 0; - - 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 this.LOAD_BASE&0xff; - case 2: return (this.LOAD_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) { - this.loadRAMWithProgram(); - } - 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: this.ram.slice(0,0) // unused - }; - } - 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> 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 this.LOAD_BASE & 0xff; + case 2: return (this.LOAD_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) { + this.loadRAMWithProgram(); + } + 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: this.ram.slice(0, 0) // unused + }; + } + 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..."); + 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); - } + } loadROM(data) { // is it a 16-sector 35-track disk image? if (data.length == 16 * 35 * 256) { @@ -209,27 +208,27 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { this.ram[0xbf6f] = 0x01; } } - reset() { - this.auxRAMselected = false; - this.auxRAMbank = 1; - this.writeinhibit = true; - this.ram.fill(0, 0x300, 0x400); // Clear soft-reset vector - // (force hard reset) - super.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; - } - } + reset() { + this.auxRAMselected = false; + this.auxRAMbank = 1; + this.writeinhibit = true; + this.ram.fill(0, 0x300, 0x400); // Clear soft-reset vector + // (force hard reset) + super.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]; @@ -247,459 +246,444 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { 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; + 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; + } else if (address < 0xc090) { + 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); + this.probe.logIOWrite(address, 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: case 91: + return; // ignore shift/ctrl/alt - don't set any key + 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: code = (flags & KeyFlags.Shift) ? 0x29 : 0x30; break; // ) or 0 + case 49: code = (flags & KeyFlags.Shift) ? 0x21 : 0x31; break; // ! or 1 + case 50: code = (flags & KeyFlags.Shift) ? 0x40 : 0x32; break; // @ or 2 + case 51: code = (flags & KeyFlags.Shift) ? 0x23 : 0x33; break; // # or 3 + case 52: code = (flags & KeyFlags.Shift) ? 0x24 : 0x34; break; // $ or 4 + case 53: code = (flags & KeyFlags.Shift) ? 0x25 : 0x35; break; // % or 5 + case 54: code = (flags & KeyFlags.Shift) ? 0x5e : 0x36; break; // ^ or 6 + case 55: code = (flags & KeyFlags.Shift) ? 0x26 : 0x37; break; // & or 7 + case 56: code = (flags & KeyFlags.Shift) ? 0x2a : 0x38; break; // * or 8 + case 57: code = (flags & KeyFlags.Shift) ? 0x28 : 0x39; break; // ( or 9 + case 61: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _ or - + case 173: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // + or = + case 59: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break; // : or ; + case 186: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break; // : or ; + case 187: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // + or = + case 188: code = (flags & KeyFlags.Shift) ? 0x3c : 0x2c; break; + case 189: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _ or - + 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) { + // TODO: require two writes in a row for some things + switch (address & 0x0f) { + // Select aux RAM bank 2, write protected. + case 0x0: + case 0x4: + this.auxRAMselected = true; + this.auxRAMbank = 2; + this.writeinhibit = true; break; - case 3: - this.soundstate = this.soundstate ^ 1; + // Select ROM, write enable aux RAM bank 2. + case 0x1: + case 0x5: + this.auxRAMselected = false; + this.auxRAMbank = 2; + this.writeinhibit = false; 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)); - } + // 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; - 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; - } else if (address < 0xc090) { - 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); - this.probe.logIOWrite(address, val); - } else if (address >= 0xd000 && !this.writeinhibit) { - if (address >= 0xe000) - this.ram[address] = val; + this.setupLanguageCardConstants(); + return this.floatbus(); + } + + setupLanguageCardConstants() { + // reset language card constants + if (this.auxRAMbank == 2) + this.bank2rdoffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff 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: case 91: - return; // ignore shift/ctrl/alt - don't set any key - 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: code = (flags & KeyFlags.Shift) ? 0x29 : 0x30; break; // ) or 0 - case 49: code = (flags & KeyFlags.Shift) ? 0x21 : 0x31; break; // ! or 1 - case 50: code = (flags & KeyFlags.Shift) ? 0x40 : 0x32; break; // @ or 2 - case 51: code = (flags & KeyFlags.Shift) ? 0x23 : 0x33; break; // # or 3 - case 52: code = (flags & KeyFlags.Shift) ? 0x24 : 0x34; break; // $ or 4 - case 53: code = (flags & KeyFlags.Shift) ? 0x25 : 0x35; break; // % or 5 - case 54: code = (flags & KeyFlags.Shift) ? 0x5e : 0x36; break; // ^ or 6 - case 55: code = (flags & KeyFlags.Shift) ? 0x26 : 0x37; break; // & or 7 - case 56: code = (flags & KeyFlags.Shift) ? 0x2a : 0x38; break; // * or 8 - case 57: code = (flags & KeyFlags.Shift) ? 0x28 : 0x39; break; // ( or 9 - case 61: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _ or - - case 173: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // + or = - case 59: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break; // : or ; - case 186: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break; // : or ; - case 187: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // + or = - case 188: code = (flags & KeyFlags.Shift) ? 0x3c : 0x2c; break; - case 189: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _ or - - 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; + 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"; } - } - } - - doLanguageCardIO(address:number) { - // TODO: require two writes in a row for some things - 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 = 250; - - // 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, +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 = 250; + + // 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) ]; - var colors_lut; + 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 + ]; - /** - * 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<> 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 { + 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); @@ -708,409 +692,389 @@ var Apple2Display = function(pixels : Uint32Array, apple : AppleGRParams) { 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> 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 + 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/) @@ -1120,92 +1084,92 @@ const APPLEIIGO_LZG = `TFpHAAAwAAAABYxwdy2NARUZHjRBUFBMRUlJR08gUk9NMS4wADQfNB80H /// 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 - ]; +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; + 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 { - 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>1]; - else - this.track_data = null; - } - - toLongString() { - return "Track: " + (this.track / 2) + + 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 { + } + + read_latch(): number { this.track_index = (this.track_index + 1) % TRACK_SIZE; if (this.track_data) { return (this.track_data[this.track_index] & 0xff); @@ -1218,16 +1182,14 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState 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); } + + 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) - { + 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 @@ -1244,69 +1206,66 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState phase = (address >> 1) & 3; // if new phase is even and current phase is odd - if (phase == ((new_track - 1) & 3)) - { + 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); + 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. - */ + /* + * Turn drive motor off. + */ case 0x8: this.motor = false; break; - /* - * Turn drive motor on. - */ + /* + * Turn drive motor on. + */ case 0x9: this.motor = true; - break; - /* - * Select drive 1. - */ + break; + /* + * Select drive 1. + */ case 0xa: //drive = 0; break; - /* - * Select drive 2. - */ + /* + * Select drive 2. + */ case 0xb: //drive = 1; break; - /* - * Select write mode. - */ + /* + * Select write mode. + */ case 0xf: this.read_mode = false; - /* - * Read a disk byte if read mode is active. - */ + /* + * 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. - */ + /* + * 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. - */ + /* + * 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); @@ -1322,27 +1281,27 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState /* --------------- 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 - ]; +/* + * 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. - */ +/* + * Sector skewing table. + */ - const skewing_table = [ - 0,7,14,6,13,5,12,4,11,3,10,2,9,1,8,15 - ]; +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: @@ -1356,149 +1315,143 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState * 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; - } +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 10: write the last value as the checksum. - */ - out[i++] = (byte_translation[value]); + /* + * Step 2: write the 3-byte address header (0xd5 0xaa 0x96). + */ + out[i++] = 0xd5; + out[i++] = 0xaa; + out[i++] = 0x96; - /* - * Step 11: write the 3-byte data trailer. - */ - out[i++] = (0xde); - out[i++] = (0xaa); - out[i++] = (0xeb); + /* + * 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; } - 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; + /* + * 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; +} From 361722f1ba4e0af771f2b69475cfe08414091c74 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 16:25:12 -0700 Subject: [PATCH 06/12] Add apple2 paddle/joystick support --- src/machine/apple2.ts | 73 +++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index e610e2a8..cb858f55 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -1,5 +1,5 @@ import { MOS6502, MOS6502State } from "../common/cpu/MOS6502"; -import { Bus, BasicScanlineMachine, SavesState, AcceptsBIOS } from "../common/devices"; +import { Bus, BasicScanlineMachine, SavesState, AcceptsBIOS, AcceptsPaddleInput } from "../common/devices"; import { KeyFlags } from "../common/emu"; // TODO import { hex, lzgmini, stringToByteArray, RGBA, printFlags, arrayCompare } from "../common/util"; @@ -26,7 +26,7 @@ interface SlotDevice extends Bus { readConst(address: number): number; } -export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { +export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, AcceptsPaddleInput { // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ cpuFrequency = 1022727; @@ -37,6 +37,8 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { numVisibleScanlines = 192; numTotalScanlines = 262; defaultROMSize = 0x13000; // we'll never need one that big, but... + cpuCyclesPaddleUnit = 11; // ~11 cycles per paddle unit + cpuCyclesPaddleMax = 255 * this.cpuCyclesPaddleUnit; // these are set later LOAD_BASE = 0; @@ -58,6 +60,9 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { // bank 1 is E000-FFFF, bank 2 is D000-DFFF bank2rdoffset = 0; bank2wroffset = 0; + // Paddle/joystick, PDL0-PDL3 values (0-255). + paddleValues = [0, 0, 0, 0]; + paddleLastTriggered = 0; // disk II slots: SlotDevice[] = new Array(8); // fake disk drive that loads program into RAM @@ -254,15 +259,15 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { this.probe.logIORead(address, 0); // TODO: value var slot = (address >> 4) & 0x0f; switch (slot) { - case 0: + case 0: // $C00x return this.kbdlatch; - case 1: + case 1: // $C01x this.kbdlatch &= 0x7f; break; - case 3: + case 3: // $C03x this.soundstate = this.soundstate ^ 1; break; - case 5: + case 5: // $C05x if ((address & 0x0f) < 8) { // graphics if ((address & 1) != 0) @@ -271,28 +276,43 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { this.grparams.grswitch &= ~(1 << ((address >> 1) & 0x07)); } break; - case 6: + case 6: // $C06x // tapein, joystick, buttons switch (address & 7) { - // buttons (off) - case 1: - case 2: - case 3: + case 1: // $C061 / $C069 - GAME SW0 + case 2: // $C062 / $C06A - GAME SW1 + case 3: // $C063 / $C06B - GAME SW2 + // buttons (off) return this.floatbus() & 0x7f; - // joystick - case 4: - case 5: - return this.floatbus() | 0x80; + case 4: // $C064 / $C06C - GAME PDL0 + case 5: // $C065 / $C06D - GAME PDL1 + case 6: // $C066 / $C06E - GAME PDL2 + case 7: // $C067 / $C06F - GAME PDL3 + var pdl = (address & 0x0f) - 4; + var elapsed = this.frameCycles - this.paddleLastTriggered; + // Bit 7 remains high until paddle value reached. + if (elapsed < this.paddleValues[pdl] * this.cpuCyclesPaddleUnit) + return this.floatbus() | 0x80; + else + return this.floatbus() & 0x7f; default: return this.floatbus(); } - case 7: - // joy reset - if (address == 0xc070) + case 7: // $C07x + // Strobing PTRIG ($C070) triggers paddles. + if (address == 0xc070) { + this.paddleLastTriggered = this.frameCycles; return this.floatbus() | 0x80; - case 8: + } + case 8: // $C08x return this.doLanguageCardIO(address); - case 9: case 10: case 11: case 12: case 13: case 14: case 15: + case 9: // $C09x + case 10: // $C0Ax + case 11: // $C0Bx + case 12: // $C0Cx + case 13: // $C0Dx + case 14: // $C0Ex + case 15: // $C0Fx return (this.slots[slot - 8] && this.slots[slot - 8].read(address & 0xf)) | 0; } } else if (address >= 0xc100 && address < 0xc800) { @@ -347,6 +367,13 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { this.ap2disp && this.ap2disp.updateScreen(); return clocks; } + postFrame() { + this.paddleLastTriggered -= this.frameCycles; + // Prevent extreme negative values if paddles aren't read. + if (this.paddleLastTriggered < -this.cpuCyclesPaddleMax) { + this.paddleLastTriggered = -this.cpuCyclesPaddleMax; + } + } advanceCPU() { this.audio.feedSample(this.soundstate, 1); return super.advanceCPU(); @@ -408,6 +435,12 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { } } + setPaddleInput(controller: number, value: number): void { + if (controller >= 0 && controller < this.paddleValues.length) { + this.paddleValues[controller] = value & 0xff; + } + } + doLanguageCardIO(address: number) { // TODO: require two writes in a row for some things switch (address & 0x0f) { From f159ebcafbfa91cfe79c9b33a759108d1ca0a035 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 18:34:58 -0700 Subject: [PATCH 07/12] Add Apple SW0/SW1/SW2 button support --- src/common/baseplatform.ts | 5 +++++ src/common/devices.ts | 1 + src/common/emu.ts | 9 +++++++++ src/machine/apple2.ts | 15 +++++++++++++-- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index 2fced0af..9fdd4be1 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -881,6 +881,11 @@ export abstract class BaseMachinePlatform extends BaseDebugPl if (hasPaddleInput(this.machine)) { this.machine.setPaddleInput(0, this.video.paddle_x); this.machine.setPaddleInput(1, this.video.paddle_y); + if (this.machine.setPaddleButton) { + this.machine.setPaddleButton(0, this.video.paddle_buttons[0]); + this.machine.setPaddleButton(1, this.video.paddle_buttons[1]); + this.machine.setPaddleButton(2, this.video.paddle_buttons[2]); + } } // TODO: put into interface if (this.machine['pollControls']) { diff --git a/src/common/devices.ts b/src/common/devices.ts index d22848fe..fd834088 100644 --- a/src/common/devices.ts +++ b/src/common/devices.ts @@ -112,6 +112,7 @@ export interface AcceptsKeyInput { export interface AcceptsPaddleInput { setPaddleInput(controller: number, value: number): void; + setPaddleButton?(index: number, pressed: boolean): void; } // TODO: interface not yet used (setKeyInput() handles joystick) diff --git a/src/common/emu.ts b/src/common/emu.ts index 902f0d26..ccc592a1 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -86,6 +86,8 @@ export class RasterVideo { // Start paddles/joystick centered in [0,255] range. paddle_x = 128; paddle_y = 128; + // Platforms can support up to three buttons. + paddle_buttons: boolean[] = [false, false, false]; setRotate(rotate: number) { var canvas = this.canvas; @@ -148,6 +150,13 @@ export class RasterVideo { var new_y = Math.round(pos.y * 255 / this.canvas.height); this.paddle_x = clamp(0, 255, new_x); this.paddle_y = clamp(0, 255, new_y); + }).mousedown((e) => { + // TODO Allows users to specify mapping in settings. + this.paddle_buttons[0] = !e.shiftKey && !e.altKey; + this.paddle_buttons[1] = e.shiftKey && !e.altKey; + this.paddle_buttons[2] = e.altKey && !e.shiftKey; + }).mouseup((e) => { + this.paddle_buttons.fill(false); }); }; } diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index cb858f55..8a75d0de 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -62,6 +62,7 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, Accept bank2wroffset = 0; // Paddle/joystick, PDL0-PDL3 values (0-255). paddleValues = [0, 0, 0, 0]; + paddleButtons = [false, false, false]; paddleLastTriggered = 0; // disk II slots: SlotDevice[] = new Array(8); @@ -282,8 +283,12 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, Accept case 1: // $C061 / $C069 - GAME SW0 case 2: // $C062 / $C06A - GAME SW1 case 3: // $C063 / $C06B - GAME SW2 - // buttons (off) - return this.floatbus() & 0x7f; + // buttons + var btn = (address & 7) - 1; + if (this.paddleButtons[btn]) + return this.floatbus() | 0x80; + else + return this.floatbus() & 0x7f; case 4: // $C064 / $C06C - GAME PDL0 case 5: // $C065 / $C06D - GAME PDL1 case 6: // $C066 / $C06E - GAME PDL2 @@ -441,6 +446,12 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, Accept } } + setPaddleButton(index: number, pressed: boolean): void { + if (index >= 0 && index < this.paddleButtons.length) { + this.paddleButtons[index] = pressed; + } + } + doLanguageCardIO(address: number) { // TODO: require two writes in a row for some things switch (address & 0x0f) { From 9725a3f45c20d94c8022820b678e572f705618ae Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 18:47:07 -0700 Subject: [PATCH 08/12] Apple load/save paddle state --- src/machine/apple2.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index 8a75d0de..c7507268 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -19,6 +19,8 @@ interface AppleIIState extends AppleIIStateBase, AppleIIControlsState { c: MOS6502State; grswitch: number; slots: SlotDevice[]; + paddleValues: number[]; + paddleButtons: boolean[]; } interface SlotDevice extends Bus { @@ -140,6 +142,8 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, Accept auxRAMbank: this.auxRAMbank, writeinhibit: this.writeinhibit, slots: this.slots.map((slot) => { return slot && slot['saveState'] && slot['saveState']() }), + paddleValues: this.paddleValues.slice(), + paddleButtons: this.paddleButtons.slice(), inputs: this.ram.slice(0, 0) // unused }; } @@ -156,6 +160,8 @@ export class AppleII extends BasicScanlineMachine implements AcceptsBIOS, Accept 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.paddleValues = s.paddleValues.slice(); + this.paddleButtons = s.paddleButtons.slice(); this.ap2disp.invalidate(); // repaint entire screen } saveControlsState(): AppleIIControlsState { From a5e1c0359dba42a9215f00e0205bdda9a2677fb0 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 21 Mar 2026 22:07:24 -0700 Subject: [PATCH 09/12] Add emucontrols-apple2 --- index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.html b/index.html index 78f71ba4..b85dbdfc 100644 --- a/index.html +++ b/index.html @@ -321,6 +321,13 @@ ←↑↓→ Joystick Space Button +