diff --git a/js/apple2.ts b/js/apple2.ts index 64ec1c3..b541786 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -1,9 +1,9 @@ import Apple2IO from './apple2io'; -// import * as gl from './gl'; import { HiresPage, LoresPage, VideoModes, + VideoModesState, } from './videomodes'; import { HiresPage2D, @@ -15,9 +15,11 @@ import { LoresPageGL, VideoModesGL, } from './gl'; -import CPU6502, { PageHandler, CpuState } from './cpu6502'; -import MMU from './mmu'; -import RAM from './ram'; +import ROM from './roms/rom'; +import { Apple2IOState } from './apple2io'; +import CPU6502, { CpuState } from './cpu6502'; +import MMU, { MMUState } from './mmu'; +import RAM, { RAMState } from './ram'; import { debug } from './util'; import SYMBOLS from './symbols'; @@ -29,13 +31,17 @@ interface Options { enhanced: boolean, e: boolean, gl: boolean, - rom: PageHandler, + rom: ROM, canvas: HTMLCanvasElement, tick: () => void, } interface State { cpu: CpuState, + vm: VideoModesState, + io: Apple2IOState, + mmu?: MMUState, + ram?: RAMState[], } export class Apple2 implements Restorable { @@ -57,7 +63,8 @@ export class Apple2 implements Restorable { private vm: VideoModes; private io: Apple2IO; - private mmu: MMU; + private mmu: MMU | undefined; + private ram: [RAM, RAM, RAM] | undefined; private tick: () => void; @@ -85,17 +92,19 @@ export class Apple2 implements Restorable { this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, options.rom); this.cpu.addPageHandler(this.mmu); } else { - const ram1 = new RAM(0x00, 0x03); - const ram2 = new RAM(0x0C, 0x1F); - const ram3 = new RAM(0x60, 0xBF); + this.ram = [ + new RAM(0x00, 0x03), + new RAM(0x0C, 0x1F), + new RAM(0x60, 0xBF) + ]; - this.cpu.addPageHandler(ram1); + this.cpu.addPageHandler(this.ram[0]); this.cpu.addPageHandler(this.gr); this.cpu.addPageHandler(this.gr2); - this.cpu.addPageHandler(ram2); + this.cpu.addPageHandler(this.ram[1]); this.cpu.addPageHandler(this.hgr); this.cpu.addPageHandler(this.hgr2); - this.cpu.addPageHandler(ram3); + this.cpu.addPageHandler(this.ram[2]); this.cpu.addPageHandler(this.io); this.cpu.addPageHandler(options.rom); } @@ -184,6 +193,10 @@ export class Apple2 implements Restorable { getState(): State { const state: State = { cpu: this.cpu.getState(), + vm: this.vm.getState(), + io: this.io.getState(), + mmu: this.mmu?.getState(), + ram: this.ram?.map(bank => bank.getState()), }; return state; @@ -191,6 +204,18 @@ export class Apple2 implements Restorable { setState(state: State) { this.cpu.setState(state.cpu); + this.vm.setState(state.vm); + this.io.setState(state.io); + if (this.mmu && state.mmu) { + this.mmu.setState(state.mmu); + } + if (this.ram) { + this.ram.forEach((bank, idx) => { + if (state.ram) { + bank.setState(state.ram[idx]); + } + }); + } } reset() { diff --git a/js/apple2io.ts b/js/apple2io.ts index c0285c6..ff6c02b 100644 --- a/js/apple2io.ts +++ b/js/apple2io.ts @@ -10,7 +10,7 @@ */ import CPU6502, { PageHandler } from './cpu6502'; -import { Card, Memory, TapeData, byte } from './types'; +import { Card, Memory, TapeData, byte, Restorable } from './types'; import { debug } from './util'; type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; @@ -25,8 +25,9 @@ interface Annunciators { 3: boolean, } -interface State { +export interface Apple2IOState { annunciators: Annunciators; + cards: Array } export type SampleListener = (sample: number[]) => void; @@ -65,8 +66,8 @@ const LOC = { ACCEL: 0x74, // CPU Speed control }; -export default class Apple2IO implements PageHandler { - private _slot: Card[] = []; +export default class Apple2IO implements PageHandler, Restorable { + private _slot: Array = new Array(7).fill(null); private _auxRom: Memory | null = null; private _khz = 1023; @@ -139,6 +140,7 @@ export default class Apple2IO implements PageHandler { _access(off: byte, val?: byte): byte | undefined { let result: number | undefined = 0; const now = this.cpu.getCycles(); + const writeMode = val === undefined; const delta = now - this._trigger; switch (off) { case LOC.CLRTEXT: @@ -256,7 +258,9 @@ export default class Apple2IO implements PageHandler { result = this._key; break; case LOC.STROBE: // C01x - this._key &= 0x7f; + if (off === LOC.STROBE || writeMode) { + this._key &= 0x7f; + } if (this._buffer.length > 0) { let val = this._buffer.shift() as string; if (val == '\n') { @@ -264,7 +268,10 @@ export default class Apple2IO implements PageHandler { } this._key = val.charCodeAt(0) | 0x80; } - result = (this._keyDown ? 0x80 : 0x00) | this._key; + result = this._key & 0x7f; + if (off === LOC.STROBE) { + result |= this._keyDown ? 0x80 : 0x00; + } break; case LOC.TAPEOUT: // C02x this._phase = -this._phase; @@ -401,14 +408,17 @@ export default class Apple2IO implements PageHandler { } } - getState() { + getState(): Apple2IOState { + // TODO vet more potential state return { - annunciators: this._annunciators[0] + annunciators: this._annunciators, + cards: this._slot.map((card) => card ? card.getState() : null) }; } - setState(state: State) { + setState(state: Apple2IOState) { this._annunciators = state.annunciators; + state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState)); } setSlot(slot: slot, card: Card) { @@ -453,7 +463,7 @@ export default class Apple2IO implements PageHandler { } } - setTape(tape: TapeData) { // TODO(flan): Needs typing. + setTape(tape: TapeData) { debug('Tape length: ' + tape.length); this._tape = tape; this._tapeOffset = -1; @@ -470,9 +480,7 @@ export default class Apple2IO implements PageHandler { tick() { this._tick(); for (let idx = 0; idx < 8; idx++) { - if (this._slot[idx]) { - this._slot[idx].tick?.(); - } + this._slot[idx]?.tick?.(); } } diff --git a/js/base64.ts b/js/base64.ts index 188080f..4fc8e36 100644 --- a/js/base64.ts +++ b/js/base64.ts @@ -121,3 +121,27 @@ export function base64_decode(data: string | null | undefined): memory | undefin return new Uint8Array(tmp_arr); } + +const DATA_URL_PREFIX = 'data:application/octet-stream;base64,'; + +export function base64_json_parse(json: string) { + const reviver = (_key: string, value: any) => { + if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) { + return base64_decode(value.slice(DATA_URL_PREFIX.length)); + } + return value; + }; + + return JSON.parse(json, reviver); +} + +export function base64_json_stringify(json: any) { + const replacer = (_key: string, value: any) => { + if (value instanceof Uint8Array) { + return DATA_URL_PREFIX + base64_encode(value); + } + return value; + }; + + return JSON.stringify(json, replacer); +} diff --git a/js/canvas.ts b/js/canvas.ts index bb10e73..7a27cf4 100644 --- a/js/canvas.ts +++ b/js/canvas.ts @@ -9,7 +9,6 @@ * implied warranty. */ -import { base64_decode, base64_encode } from './base64'; import { byte, memory, Memory } from './types'; import { allocMemPages } from './util'; import { @@ -468,16 +467,16 @@ export class LoresPage2D implements LoresPage { page: this.page, mono: this._monoMode, buffer: [ - base64_encode(this._buffer[0]), - base64_encode(this._buffer[1]) + new Uint8Array(this._buffer[0]), + new Uint8Array(this._buffer[1]), ] }; } setState(state: GraphicsState) { this.page = state.page; this._monoMode = state.mono; - this._buffer[0] = base64_decode(state.buffer[0]); - this._buffer[1] = base64_decode(state.buffer[1]); + this._buffer[0] = new Uint8Array(state.buffer[0]); + this._buffer[1] = new Uint8Array(state.buffer[1]); this.refresh(); } @@ -879,8 +878,8 @@ export class HiresPage2D implements HiresPage { page: this.page, mono: this._monoMode, buffer: [ - base64_encode(this._buffer[0]), - base64_encode(this._buffer[1]) + new Uint8Array(this._buffer[0]), + new Uint8Array(this._buffer[1]), ] }; } @@ -888,8 +887,8 @@ export class HiresPage2D implements HiresPage { setState(state: GraphicsState) { this.page = state.page; this._monoMode = state.mono; - this._buffer[0] = base64_decode(state.buffer[0]); - this._buffer[1] = base64_decode(state.buffer[1]); + this._buffer[0] = new Uint8Array(state.buffer[0]); + this._buffer[1] = new Uint8Array(state.buffer[1]); this.refresh(); } diff --git a/js/cards/cffa.js b/js/cards/cffa.js index 4e06cdb..236d039 100644 --- a/js/cards/cffa.js +++ b/js/cards/cffa.js @@ -372,6 +372,13 @@ export default function CFFA() { } }, + getState() { + // TODO CFFA State + return {}; + }, + + setState(_) {}, + // Assign a raw disk image to a drive. Must be 2mg or raw PO image. setBinary: function(drive, name, ext, rawData) { diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 6ca1d9d..2059651 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -9,7 +9,7 @@ * implied warranty. */ -import { base64_decode, base64_encode } from '../base64'; +import { base64_decode, base64_encode} from '../base64'; import { bit, byte, DiskFormat, MemberOf, memory, nibble, rom } from '../types'; import { debug, toHex } from '../util'; import { Disk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; @@ -120,21 +120,21 @@ type Phase = 0 | 1 | 2 | 3; * activated. For example, if in phase 0 (top row), turning on phase 3 would * step backwards a quarter track while turning on phase 2 would step forwards * a half track. - * + * * Note that this emulation is highly simplified as it only takes into account * the order that coils are powered on and ignores when they are powered off. * The actual hardware allows for multiple coils to be powered at the same time * providing different levels of torque on the head arm. Along with that, the * RWTS uses a complex delay system to drive the coils faster based on expected * head momentum. - * + * * Examining the https://computerhistory.org/blog/apple-ii-dos-source-code/, * one finds that the SEEK routine on line 4831 of `appdos31.lst`. It uses * `ONTABLE` and `OFFTABLE` (each 12 bytes) to know exactly how many * microseconds to power on/off each coil as the head accelerates. At the end, * the final coil is left powered on 9.5 milliseconds to ensure the head has * settled. - * + * * https://embeddedmicro.weebly.com/apple-2iie.html shows traces of the boot * seek (which is slightly different) and a regular seek. */ @@ -151,6 +151,7 @@ type DriveNumber = MemberOf; interface Callbacks { driveLight: (drive: DriveNumber, on: boolean) => void; dirty: (drive: DriveNumber, dirty: boolean) => void; + label: (drive: DriveNumber, name: string) => void; } /** Common information for Nibble and WOZ disks. */ @@ -159,6 +160,8 @@ interface BaseDrive { format: DiskFormat, /** Current disk volume number. */ volume: byte, + /** Displayed disk name */ + name: string, /** Quarter track position of read/write head. */ track: byte, /** Position of the head on the track. */ @@ -195,7 +198,8 @@ function isNibbleDrive(drive: Drive): drive is NibbleDrive { interface DriveState { format: DiskFormat, volume: byte, - tracks: string[], + name: string, + tracks: memory[], track: byte, head: byte, phase: Phase, @@ -217,7 +221,8 @@ function getDriveState(drive: Drive): DriveState { const result: DriveState = { format: drive.format, volume: drive.volume, - tracks: [] as string[], + name: drive.name, + tracks: [], track: drive.track, head: drive.head, phase: drive.phase, @@ -228,7 +233,7 @@ function getDriveState(drive: Drive): DriveState { throw Error('No tracks.'); } for (let idx = 0; idx < drive.tracks.length; idx++) { - result.tracks.push(base64_encode(drive.tracks[idx])); + result.tracks.push(new Uint8Array(drive.tracks[idx])); } return result; } @@ -238,6 +243,7 @@ function setDriveState(state: DriveState) { const result: Drive = { format: state.format, volume: state.volume, + name: state.name, tracks: [] as memory[], track: state.track, head: state.head, @@ -246,8 +252,9 @@ function setDriveState(state: DriveState) { dirty: state.dirty }; for (let idx = 0; idx < state.tracks.length; idx++) { - result.tracks!.push(base64_decode(state.tracks[idx])); + result.tracks!.push(new Uint8Array(state.tracks[idx])); } + return result; } @@ -260,6 +267,7 @@ export default class DiskII { { // Drive 1 format: 'dsk', volume: 254, + name: 'Disk 1', tracks: [], track: 0, head: 0, @@ -270,6 +278,7 @@ export default class DiskII { { // Drive 2 format: 'dsk', volume: 254, + name: 'Disk 2', tracks: [], track: 0, head: 0, @@ -290,7 +299,7 @@ export default class DiskII { /** Q7 (Read/Write): Used by WOZ disks. */ private q7: boolean = false; /** Q7 (Read/Write): Used by Nibble disks. */ - private writeMode = false; + private writeMode = false; /** Whether the selected drive is on. */ private on = false; /** Current drive number (0, 1). */ @@ -521,9 +530,7 @@ export default class DiskII { this.offTimeout = window.setTimeout(() => { this.debug('Drive Off'); this.on = false; - if (this.callbacks.driveLight) { - this.callbacks.driveLight(this.drive, false); - } + this.callbacks.driveLight(this.drive, false); }, 1000); } } @@ -538,9 +545,7 @@ export default class DiskII { this.debug('Drive On'); this.on = true; this.lastCycles = this.io.cycles(); - if (this.callbacks.driveLight) { - this.callbacks.driveLight(this.drive, true); - } + this.callbacks.driveLight(this.drive, true); } break; @@ -548,7 +553,7 @@ export default class DiskII { this.debug('Disk 1'); this.drive = 1; this.cur = this.drives[this.drive - 1]; - if (this.on && this.callbacks.driveLight) { + if (this.on) { this.callbacks.driveLight(2, false); this.callbacks.driveLight(1, true); } @@ -557,7 +562,7 @@ export default class DiskII { this.debug('Disk 2'); this.drive = 2; this.cur = this.drives[this.drive - 1]; - if (this.on && this.callbacks.driveLight) { + if (this.on) { this.callbacks.driveLight(1, false); this.callbacks.driveLight(2, true); } @@ -685,9 +690,11 @@ export default class DiskII { this.on = state.on; this.drive = state.drive; for (const d of DRIVE_NUMBERS) { - this.drives[d - 1] = setDriveState(state.drives[d - 1]); + const idx = d - 1; + this.drives[idx] = setDriveState(state.drives[idx]); + this.callbacks.label(d, state.drives[idx].name); this.callbacks.driveLight(d, this.on); - this.callbacks.dirty(d, this.drives[d - 1].dirty); + this.callbacks.dirty(d, this.drives[idx].dirty); } this.cur = this.drives[this.drive - 1]; } @@ -777,6 +784,7 @@ export default class DiskII { Object.assign(cur, newDisk); this.updateDirty(this.drive, false); + this.callbacks.label(this.drive, name); } getJSON(drive: DriveNumber, pretty: boolean) { @@ -831,6 +839,7 @@ export default class DiskII { Object.assign(cur, disk); this.updateDirty(drive, true); + this.callbacks.label(this.drive, name); return true; } @@ -881,4 +890,4 @@ export default class DiskII { } return data; } -} \ No newline at end of file +} diff --git a/js/cards/nsc.js b/js/cards/nsc.js index 7ff0757..9d6ea0e 100644 --- a/js/cards/nsc.js +++ b/js/cards/nsc.js @@ -115,5 +115,12 @@ export default function NoSlotClock(rom) _access(off); rom.write(page, off, val); }, + + getState() { + return {}; + }, + + setState(_) { + } }; } diff --git a/js/cards/parallel.js b/js/cards/parallel.js index 7fc87b2..fc9c1e6 100644 --- a/js/cards/parallel.js +++ b/js/cards/parallel.js @@ -39,6 +39,10 @@ export default function Parallel(io, cbs) { read: function(page, off) { return rom[off]; }, - write: function() {} + write: function() {}, + getState() { + return {}; + }, + setState(_) {} }; } diff --git a/js/cards/ramfactor.js b/js/cards/ramfactor.js index c060e91..186328a 100644 --- a/js/cards/ramfactor.js +++ b/js/cards/ramfactor.js @@ -9,7 +9,6 @@ * implied warranty. */ -import { base64_decode, base64_encode } from '../base64'; import { allocMem, debug } from '../util'; import { rom } from '../roms/cards/ramfactor'; @@ -140,14 +139,14 @@ export default function RAMFactor(io, size) { return { loc: _loc, firmware: _firmware, - mem: base64_encode(mem) + mem: new Uint8Array(mem) }; }, setState: function(state) { _loc = state.loc; _firmware = state.firmware; - mem = base64_decode(state.mem); + mem = new Uint8Array(state.mem); _ramhi = (_loc >> 16) & 0xff; _rammid = (_loc >> 8) & 0xff; diff --git a/js/cards/smartport.js b/js/cards/smartport.js index ebde372..1095cea 100644 --- a/js/cards/smartport.js +++ b/js/cards/smartport.js @@ -9,7 +9,6 @@ * implied warranty. */ -import { base64_decode } from '../base64'; import { debug, toHex } from '../util'; import { rom } from '../roms/cards/smartport'; @@ -21,7 +20,6 @@ export default function SmartPort(io, cpu, options ) { var BLOCK_LO = 0x46; // var BLOCK_HI = 0x47; - var disks = []; function _init() { @@ -36,7 +34,7 @@ export default function SmartPort(io, cpu, options ) { function decodeDisk(unit, disk) { disks[unit] = []; for (var idx = 0; idx < disk.blocks.length; idx++) { - disks[unit][idx] = base64_decode(disk.blocks[idx]); + disks[unit][idx] = new Uint8Array(disk.blocks[idx]); } } @@ -413,9 +411,21 @@ export default function SmartPort(io, cpu, options ) { }, getState: function() { + return { + disks: disks.map( + (disk) => disk.map( + (block) => new Uint8Array(block) + ) + ) + }; }, - setState: function() { + setState: function(state) { + disks = state.disks.map( + (disk) => disk.map( + (block) => new Uint8Array(block) + ) + ); }, setBinary: function (drive, name, fmt, data) { diff --git a/js/cards/thunderclock.js b/js/cards/thunderclock.js index 6601fc3..04ad21f 100644 --- a/js/cards/thunderclock.js +++ b/js/cards/thunderclock.js @@ -150,6 +150,10 @@ export default function Thunderclock() }, ioSwitch: function thunderclock_ioSwitch(off, val) { return _access(off, val); - } + }, + getState() { + return {}; + }, + setState(_) {} }; } diff --git a/js/cards/videoterm.js b/js/cards/videoterm.js index 43460b6..d3bcc50 100644 --- a/js/cards/videoterm.js +++ b/js/cards/videoterm.js @@ -256,6 +256,29 @@ export default function Videoterm(_io) { return _imageData; } return; + }, + + getState() { + return { + curReg: _curReg, + startPos: _startPos, + cursorPos: _cursorPos, + bank: _bank, + buffer: new Uint8Array(_buffer), + regs: [..._regs], + }; + }, + + setState(state) { + _curReg = state.curReg; + _startPos = state.startPos; + _cursorPos = state.cursorPos; + _bank = state.bank; + _buffer = new Uint8Array(_buffer); + _regs = [...state.regs]; + + _shouldRefresh = true; + _dirty = true; } }; } diff --git a/js/gl.ts b/js/gl.ts index f014269..72f90c4 100644 --- a/js/gl.ts +++ b/js/gl.ts @@ -9,7 +9,6 @@ * implied warranty. */ -import { base64_decode, base64_encode } from './base64'; import { byte, memory, Memory, Restorable } from './types'; import { allocMemPages } from './util'; @@ -340,16 +339,16 @@ export class LoresPageGL implements LoresPage { page: this.page, mono: this._monoMode, buffer: [ - base64_encode(this._buffer[0]), - base64_encode(this._buffer[1]) + new Uint8Array(this._buffer[0]), + new Uint8Array(this._buffer[1]), ] }; } setState(state: GraphicsState) { this.page = state.page; - this._buffer[0] = base64_decode(state.buffer[0]); - this._buffer[1] = base64_decode(state.buffer[1]); + this._buffer[0] = new Uint8Array(state.buffer[0]); + this._buffer[1] = new Uint8Array(state.buffer[1]); this.refresh(); } @@ -646,16 +645,16 @@ export class HiresPageGL implements Memory, Restorable { page: this.page, mono: this._monoMode, buffer: [ - base64_encode(this._buffer[0]), - base64_encode(this._buffer[1]) + new Uint8Array(this._buffer[0]), + new Uint8Array(this._buffer[1]), ] }; } setState(state: GraphicsState) { this.page = state.page; - this._buffer[0] = base64_decode(state.buffer[0]); - this._buffer[1] = base64_decode(state.buffer[1]); + this._buffer[0] = new Uint8Array(state.buffer[0]); + this._buffer[1] = new Uint8Array(state.buffer[1]); this.refresh(); } @@ -914,6 +913,7 @@ export class VideoModesGL implements VideoModes { this._grs[1].setState(state.grs[1]); this._hgrs[0].setState(state.hgrs[0]); this._hgrs[1].setState(state.hgrs[1]); + this._refresh(); } mono(on: boolean) { diff --git a/js/mmu.ts b/js/mmu.ts index fd15508..2c17940 100644 --- a/js/mmu.ts +++ b/js/mmu.ts @@ -10,9 +10,10 @@ */ import CPU6502 from './cpu6502'; -import RAM from './ram'; +import RAM, { RAMState } from './ram'; +import ROM, { ROMState } from './roms/rom'; import { debug, toHex } from './util'; -import { byte, Memory } from './types'; +import { byte, Memory, Restorable } from './types'; import Apple2IO from './apple2io'; import { HiresPage, LoresPage, VideoModes } from './videomodes'; @@ -138,10 +139,18 @@ class Switches implements Memory { } } -class AuxRom { +class AuxRom implements Memory { constructor( private readonly mmu: MMU, - private readonly rom: Memory) { } + private readonly rom: ROM) { } + + start() { + return 0xc1; + } + + end() { + return 0xcf; + } read(page: byte, off: byte) { if (page == 0xc3) { @@ -160,38 +169,33 @@ class AuxRom { } } -/* -interface State { - bank1: this._bank1, - readbsr: this._readbsr, - writebsr: this._writebsr, - prewrite: this._prewrite, +export interface MMUState { + bank1: boolean + readbsr: boolean + writebsr: boolean + prewrite: boolean - intcxrom: this._intcxrom, - slot3rom: this._slot3rom, - intc8rom: this._intc8rom, + intcxrom: boolean + slot3rom: boolean + intc8rom: boolean - auxRamRead: this._auxRamRead, - auxRamWrite: this._auxRamWrite, - altzp: this._altzp, + auxRamRead: boolean + auxRamWrite: boolean + altzp: boolean - _80store: this._80store, - page2: this._page2, - hires: this._hires, + _80store: boolean + page2: boolean + hires: boolean - mem00_01: [this.mem00_01[0].getState(), this.mem00_01[1].getState()], - mem02_03: [this.mem02_03[0].getState(), this.mem02_03[1].getState()], - mem0C_1F: [this.mem0C_1F[0].getState(), this.mem0C_1F[1].getState()], - mem60_BF: [this.mem60_BF[0].getState(), this.mem60_BF[1].getState()], - memD0_DF: [ - this.memD0_DF[0].getState(), this.memD0_DF[1].getState(), - this.memD0_DF[2].getState(), this.memD0_DF[3].getState() - ], - memE0_FF: [this.memE0_FF[0].getState(), this.memE0_FF[1].getState()] -}; -*/ + mem00_01: [RAMState, RAMState] + mem02_03: [RAMState, RAMState] + mem0C_1F: [RAMState, RAMState] + mem60_BF: [RAMState, RAMState] + memD0_DF: [ROMState, RAMState, RAMState, RAMState, RAMState] + memE0_FF: [ROMState, RAMState, RAMState] +} -export default class MMU implements Memory { +export default class MMU implements Memory, Restorable { private _readPages = new Array(0x100); private _writePages = new Array(0x100); private _pages = new Array(0x100); @@ -235,12 +239,15 @@ export default class MMU implements Memory { private mem60_BF = [new RAM(0x60, 0xBF), new RAM(0x60, 0xBF)]; private memC0_C0 = [this.switches]; private memC1_CF = [this.io, this.auxRom]; - private memD0_DF = [ + private memD0_DF: [ROM, RAM, RAM, RAM, RAM] = [ this.rom, new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF) ]; - private memE0_FF = [this.rom, new RAM(0xE0, 0xFF), new RAM(0xE0, 0xFF)]; + private memE0_FF: [ROM, RAM, RAM] = [ + this.rom, + new RAM(0xE0, 0xFF), new RAM(0xE0, 0xFF) + ]; constructor( private readonly cpu: CPU6502, @@ -250,8 +257,7 @@ export default class MMU implements Memory { private readonly hires1: HiresPage, private readonly hires2: HiresPage, private readonly io: Apple2IO, - // TODO(flan): Better typing. - private readonly rom: any) { + private readonly rom: ROM) { /* * Initialize read/write banks */ @@ -332,7 +338,7 @@ export default class MMU implements Memory { } _initSwitches() { - this._bank1 = true; + this._bank1 = false; this._readbsr = false; this._writebsr = false; this._prewrite = false; @@ -352,7 +358,7 @@ export default class MMU implements Memory { this._iouDisable = true; } - _debug(..._args: any) { + _debug(..._args: any[]) { // debug.apply(this, arguments); } @@ -813,6 +819,9 @@ export default class MMU implements Memory { } public read(page: byte, off: byte) { + if (page === 0xff && off === 0xfc && this._intcxrom) { + this._initSwitches(); + } return this._readPages[page].read(page, off); } @@ -823,8 +832,8 @@ export default class MMU implements Memory { public resetVB() { this._vbEnd = this.cpu.getCycles() + 1000; } -/* - public getState(): State { + + public getState(): MMUState { return { bank1: this._bank1, readbsr: this._readbsr, @@ -848,14 +857,21 @@ export default class MMU implements Memory { mem0C_1F: [this.mem0C_1F[0].getState(), this.mem0C_1F[1].getState()], mem60_BF: [this.mem60_BF[0].getState(), this.mem60_BF[1].getState()], memD0_DF: [ - this.memD0_DF[0].getState(), this.memD0_DF[1].getState(), - this.memD0_DF[2].getState(), this.memD0_DF[3].getState() + this.memD0_DF[0].getState(), + this.memD0_DF[1].getState(), + this.memD0_DF[2].getState(), + this.memD0_DF[3].getState(), + this.memD0_DF[4].getState() ], - memE0_FF: [this.memE0_FF[0].getState(), this.memE0_FF[1].getState()] + memE0_FF: [ + this.memE0_FF[0].getState(), + this.memE0_FF[1].getState(), + this.memE0_FF[2].getState() + ] }; } - public setState(state: State) { + public setState(state: MMUState) { this._readbsr = state.readbsr; this._writebsr = state.writebsr; this._bank1 = state.bank1; @@ -885,10 +901,11 @@ export default class MMU implements Memory { this.memD0_DF[1].setState(state.memD0_DF[1]); this.memD0_DF[2].setState(state.memD0_DF[2]); this.memD0_DF[3].setState(state.memD0_DF[3]); + this.memD0_DF[4].setState(state.memD0_DF[4]); this.memE0_FF[0].setState(state.memE0_FF[0]); this.memE0_FF[1].setState(state.memE0_FF[1]); + this.memE0_FF[2].setState(state.memE0_FF[2]); this._updateBanks(); } -*/ } diff --git a/js/ram.ts b/js/ram.ts index 283b7b6..7483603 100644 --- a/js/ram.ts +++ b/js/ram.ts @@ -9,24 +9,19 @@ * implied warranty. */ -import { base64_decode, base64_encode } from './base64'; -import { byte, memory, Memory } from './types'; +import { byte, memory, Memory, Restorable } from './types'; import { allocMemPages } from './util'; -export interface State { - /** Start of memory region. */ - start: byte; - /** End of memory region. */ - end: byte; - /** Base64-encoded contents. */ - mem: string; +export interface RAMState { + /** Copy of contents. */ + mem: memory; } /** * Represents RAM from the start page `sp` to end page `ep`. The memory * is addressed by `page` and `offset`. */ -export default class RAM implements Memory { +export default class RAM implements Memory, Restorable { private start_page: byte; private end_page: byte; private mem: memory; @@ -54,17 +49,13 @@ export default class RAM implements Memory { this.mem[(page - this.start_page) << 8 | offset] = val; } - public getState(): State { + public getState(): RAMState { return { - start: this.start_page, - end: this.end_page, - mem: base64_encode(this.mem) + mem: new Uint8Array(this.mem) }; } - public setState(state: State) { - this.start_page = state.start; - this.end_page = state.end; - this.mem = base64_decode(state.mem); + public setState(state: RAMState) { + this.mem = new Uint8Array(state.mem); } } diff --git a/js/roms/rom.ts b/js/roms/rom.ts index 8b86080..c027532 100644 --- a/js/roms/rom.ts +++ b/js/roms/rom.ts @@ -1,7 +1,9 @@ import { PageHandler } from '../cpu6502'; -import { byte, rom } from '../types'; +import { Restorable, byte, rom } from '../types'; -export default class ROM implements PageHandler { +export type ROMState = null; + +export default class ROM implements PageHandler, Restorable { constructor( private readonly startPage: byte, @@ -22,6 +24,11 @@ export default class ROM implements PageHandler { read(page: byte, off: byte) { return this.rom[(page - this.startPage) << 8 | off]; } - write() { + write() { + } + getState() { + return null; + } + setState(_state: null) { } } diff --git a/js/types.ts b/js/types.ts index 3add0d4..fe59544 100644 --- a/js/types.ts +++ b/js/types.ts @@ -42,7 +42,7 @@ export interface Memory { } /* An interface card */ -export interface Card extends Memory { +export interface Card extends Memory, Restorable { /* Reset the card */ reset(): void; @@ -87,7 +87,7 @@ export interface DiskIIDrive extends Drive { export type TapeData = Array<[duration: number, high: boolean]>; -export interface Restorable { +export interface Restorable { getState(): T; setState(state: T): void; } diff --git a/js/ui/apple2.js b/js/ui/apple2.js index 4e9d3f3..faf35b2 100644 --- a/js/ui/apple2.js +++ b/js/ui/apple2.js @@ -1,5 +1,6 @@ import MicroModal from 'micromodal'; +import { base64_json_parse, base64_json_stringify } from '../base64'; import Audio from './audio'; import DriveLights from './drive_lights'; import { DISK_FORMATS } from '../types'; @@ -301,13 +302,11 @@ function doLoadLocalDisk(drive, file) { if (this.result.byteLength >= 800 * 1024) { if (_smartPort.setBinary(drive, name, ext, this.result)) { - driveLights.label(drive, name); focused = false; initGamepad(); } } else { if (_disk2.setBinary(drive, name, ext, this.result)) { - driveLights.label(drive, name); focused = false; initGamepad(); } @@ -362,12 +361,10 @@ export function doLoadHTTP(drive, _url) { var name = decodeURIComponent(fileParts.join('.')); if (data.byteLength >= 800 * 1024) { if (_smartPort.setBinary(drive, name, ext, data)) { - driveLights.label(drive, name); initGamepad(); } } else { if (_disk2.setBinary(drive, name, ext, data)) { - driveLights.label(drive, name); initGamepad(); } } @@ -503,7 +500,6 @@ function loadDisk(drive, disk) { disk_cur_cat[drive] = category; disk_cur_name[drive] = name; - driveLights.label(drive, name); _disk2.setDisk(drive, disk); initGamepad(disk.gamepad); } @@ -635,7 +631,7 @@ function _keydown(evt) { evt.preventDefault(); var key = keyboard.mapKeyEvent(evt); - if (key != 0xff) { + if (key !== 0xff) { io.keyDown(key); } } @@ -666,9 +662,11 @@ function _keydown(evt) { } else if (evt.keyCode === 114) { // F3 io.keyDown(0x1b); } else if (evt.keyCode === 117) { // F6 Quick Save - _apple2.getState(); + window.localStorage.state = base64_json_stringify(_apple2.getState()); } else if (evt.keyCode === 120) { // F9 Quick Restore - _apple2.setState(); + if (window.localStorage.state) { + _apple2.setState(base64_json_parse(window.localStorage.state)); + } } else if (evt.keyCode == 16) { // Shift keyboard.shiftKey(true); } else if (evt.keyCode == 20) { // Caps lock diff --git a/js/ui/drive_lights.js b/js/ui/drive_lights.js index 8fc095b..ae0ce0f 100644 --- a/js/ui/drive_lights.js +++ b/js/ui/drive_lights.js @@ -15,20 +15,6 @@ export default function DriveLights() document.querySelector('#disk-label' + drive).innerText = label; } return document.querySelector('#disk-label' + drive).innerText; - }, - getState: function() { - return { - disks: [ - this.label(1), - this.label(2) - ] - }; - }, - setState: function(state) { - if (state && state.disks) { - this.label(1, state.disks[0].label); - this.label(2, state.disks[1].label); - } } }; } diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js index 1b8606f..c342b14 100644 --- a/js/ui/keyboard.js +++ b/js/ui/keyboard.js @@ -210,7 +210,7 @@ export default function KeyBoard(cpu, io, e) { return { mapKeyEvent: function keyboard_mapKeyEvent(evt) { - var code = evt.keyCode, key = '\xff'; + var code = evt.keyCode, key = 0xff; if (evt.key in uiKitMap) { key = uiKitMap[evt.key]; @@ -230,7 +230,7 @@ export default function KeyBoard(cpu, io, e) { if (key == 0x7F && evt.shiftKey && evt.ctrlKey) { cpu.reset(); - key = '\xff'; + key = 0xff; } return key; diff --git a/js/videomodes.ts b/js/videomodes.ts index 614fb2a..f09bffc 100644 --- a/js/videomodes.ts +++ b/js/videomodes.ts @@ -1,4 +1,4 @@ -import { Memory, Restorable, byte } from './types'; +import { Memory, Restorable, byte, memory } from './types'; export type bank = 0 | 1; export type pageNo = 1 | 2; @@ -19,7 +19,7 @@ export interface Region { export interface GraphicsState { page: byte; mono: boolean; - buffer: string[]; + buffer: memory[]; } export interface VideoModesState { diff --git a/test/js/base64.test.ts b/test/js/base64.test.ts new file mode 100644 index 0000000..b667755 --- /dev/null +++ b/test/js/base64.test.ts @@ -0,0 +1,60 @@ +/** @fileoverview Test for base64.ts. */ + +import { + base64_encode, + base64_decode, + base64_json_parse, + base64_json_stringify, +} from '../../js/base64'; + +describe('base64', () => { + let memory: Uint8Array; + + beforeEach(() => { + memory = new Uint8Array([1,2,3,4,5,6]); + }); + + describe('base64_encode', () => { + it('encodes Uint8Arrays', () => { + expect(base64_encode(memory)).toEqual('AQIDBAUG'); + }); + }); + + describe('base64_decode', () => { + it('encodes Uint8Arrays', () => { + expect(base64_decode('AQIDBAUG')).toEqual(memory); + }); + }); + + describe('base64_json_parse', () => { + it('handles structures with Uint8Arrays', () => { + expect(base64_json_parse(`\ +{ + "foo": "bar", + "baz": { + "biff": "data:application/octet-stream;base64,AQIDBAUG" + } +} + `)).toEqual({ + foo: 'bar', + baz: { + biff: memory + } + }); + }); + + }); + + describe('base64_json_stringify', () => { + it('handles structures with Uint8Arrays', () => { + expect(base64_json_stringify({ + foo: 'bar', + baz: { + biff: memory + } + })).toEqual( + '{"foo":"bar","baz":{"biff":"data:application/octet-stream;base64,AQIDBAUG"}}' + ); + }); + }); +});