Flesh out some state stuff (#59)

Get save and restore state limping along to nearly as well as before I refactored and broke everything.
This commit is contained in:
Will Scullin 2021-02-27 19:17:36 -08:00 committed by GitHub
parent 983026aa1d
commit afc5280ac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 343 additions and 165 deletions

View File

@ -1,9 +1,9 @@
import Apple2IO from './apple2io'; import Apple2IO from './apple2io';
// import * as gl from './gl';
import { import {
HiresPage, HiresPage,
LoresPage, LoresPage,
VideoModes, VideoModes,
VideoModesState,
} from './videomodes'; } from './videomodes';
import { import {
HiresPage2D, HiresPage2D,
@ -15,9 +15,11 @@ import {
LoresPageGL, LoresPageGL,
VideoModesGL, VideoModesGL,
} from './gl'; } from './gl';
import CPU6502, { PageHandler, CpuState } from './cpu6502'; import ROM from './roms/rom';
import MMU from './mmu'; import { Apple2IOState } from './apple2io';
import RAM from './ram'; import CPU6502, { CpuState } from './cpu6502';
import MMU, { MMUState } from './mmu';
import RAM, { RAMState } from './ram';
import { debug } from './util'; import { debug } from './util';
import SYMBOLS from './symbols'; import SYMBOLS from './symbols';
@ -29,13 +31,17 @@ interface Options {
enhanced: boolean, enhanced: boolean,
e: boolean, e: boolean,
gl: boolean, gl: boolean,
rom: PageHandler, rom: ROM,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
tick: () => void, tick: () => void,
} }
interface State { interface State {
cpu: CpuState, cpu: CpuState,
vm: VideoModesState,
io: Apple2IOState,
mmu?: MMUState,
ram?: RAMState[],
} }
export class Apple2 implements Restorable<State> { export class Apple2 implements Restorable<State> {
@ -57,7 +63,8 @@ export class Apple2 implements Restorable<State> {
private vm: VideoModes; private vm: VideoModes;
private io: Apple2IO; private io: Apple2IO;
private mmu: MMU; private mmu: MMU | undefined;
private ram: [RAM, RAM, RAM] | undefined;
private tick: () => void; private tick: () => void;
@ -85,17 +92,19 @@ export class Apple2 implements Restorable<State> {
this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, options.rom); 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); this.cpu.addPageHandler(this.mmu);
} else { } else {
const ram1 = new RAM(0x00, 0x03); this.ram = [
const ram2 = new RAM(0x0C, 0x1F); new RAM(0x00, 0x03),
const ram3 = new RAM(0x60, 0xBF); 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.gr);
this.cpu.addPageHandler(this.gr2); this.cpu.addPageHandler(this.gr2);
this.cpu.addPageHandler(ram2); this.cpu.addPageHandler(this.ram[1]);
this.cpu.addPageHandler(this.hgr); this.cpu.addPageHandler(this.hgr);
this.cpu.addPageHandler(this.hgr2); this.cpu.addPageHandler(this.hgr2);
this.cpu.addPageHandler(ram3); this.cpu.addPageHandler(this.ram[2]);
this.cpu.addPageHandler(this.io); this.cpu.addPageHandler(this.io);
this.cpu.addPageHandler(options.rom); this.cpu.addPageHandler(options.rom);
} }
@ -184,6 +193,10 @@ export class Apple2 implements Restorable<State> {
getState(): State { getState(): State {
const state: State = { const state: State = {
cpu: this.cpu.getState(), 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; return state;
@ -191,6 +204,18 @@ export class Apple2 implements Restorable<State> {
setState(state: State) { setState(state: State) {
this.cpu.setState(state.cpu); 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() { reset() {

View File

@ -10,7 +10,7 @@
*/ */
import CPU6502, { PageHandler } from './cpu6502'; import CPU6502, { PageHandler } from './cpu6502';
import { Card, Memory, TapeData, byte } from './types'; import { Card, Memory, TapeData, byte, Restorable } from './types';
import { debug } from './util'; import { debug } from './util';
type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@ -25,8 +25,9 @@ interface Annunciators {
3: boolean, 3: boolean,
} }
interface State { export interface Apple2IOState {
annunciators: Annunciators; annunciators: Annunciators;
cards: Array<any | null>
} }
export type SampleListener = (sample: number[]) => void; export type SampleListener = (sample: number[]) => void;
@ -65,8 +66,8 @@ const LOC = {
ACCEL: 0x74, // CPU Speed control ACCEL: 0x74, // CPU Speed control
}; };
export default class Apple2IO implements PageHandler { export default class Apple2IO implements PageHandler, Restorable<Apple2IOState> {
private _slot: Card[] = []; private _slot: Array<Card | null> = new Array(7).fill(null);
private _auxRom: Memory | null = null; private _auxRom: Memory | null = null;
private _khz = 1023; private _khz = 1023;
@ -139,6 +140,7 @@ export default class Apple2IO implements PageHandler {
_access(off: byte, val?: byte): byte | undefined { _access(off: byte, val?: byte): byte | undefined {
let result: number | undefined = 0; let result: number | undefined = 0;
const now = this.cpu.getCycles(); const now = this.cpu.getCycles();
const writeMode = val === undefined;
const delta = now - this._trigger; const delta = now - this._trigger;
switch (off) { switch (off) {
case LOC.CLRTEXT: case LOC.CLRTEXT:
@ -256,7 +258,9 @@ export default class Apple2IO implements PageHandler {
result = this._key; result = this._key;
break; break;
case LOC.STROBE: // C01x case LOC.STROBE: // C01x
this._key &= 0x7f; if (off === LOC.STROBE || writeMode) {
this._key &= 0x7f;
}
if (this._buffer.length > 0) { if (this._buffer.length > 0) {
let val = this._buffer.shift() as string; let val = this._buffer.shift() as string;
if (val == '\n') { if (val == '\n') {
@ -264,7 +268,10 @@ export default class Apple2IO implements PageHandler {
} }
this._key = val.charCodeAt(0) | 0x80; 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; break;
case LOC.TAPEOUT: // C02x case LOC.TAPEOUT: // C02x
this._phase = -this._phase; this._phase = -this._phase;
@ -401,14 +408,17 @@ export default class Apple2IO implements PageHandler {
} }
} }
getState() { getState(): Apple2IOState {
// TODO vet more potential state
return { 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; this._annunciators = state.annunciators;
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
} }
setSlot(slot: slot, card: Card) { 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); debug('Tape length: ' + tape.length);
this._tape = tape; this._tape = tape;
this._tapeOffset = -1; this._tapeOffset = -1;
@ -470,9 +480,7 @@ export default class Apple2IO implements PageHandler {
tick() { tick() {
this._tick(); this._tick();
for (let idx = 0; idx < 8; idx++) { for (let idx = 0; idx < 8; idx++) {
if (this._slot[idx]) { this._slot[idx]?.tick?.();
this._slot[idx].tick?.();
}
} }
} }

View File

@ -121,3 +121,27 @@ export function base64_decode(data: string | null | undefined): memory | undefin
return new Uint8Array(tmp_arr); 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);
}

View File

@ -9,7 +9,6 @@
* implied warranty. * implied warranty.
*/ */
import { base64_decode, base64_encode } from './base64';
import { byte, memory, Memory } from './types'; import { byte, memory, Memory } from './types';
import { allocMemPages } from './util'; import { allocMemPages } from './util';
import { import {
@ -468,16 +467,16 @@ export class LoresPage2D implements LoresPage {
page: this.page, page: this.page,
mono: this._monoMode, mono: this._monoMode,
buffer: [ buffer: [
base64_encode(this._buffer[0]), new Uint8Array(this._buffer[0]),
base64_encode(this._buffer[1]) new Uint8Array(this._buffer[1]),
] ]
}; };
} }
setState(state: GraphicsState) { setState(state: GraphicsState) {
this.page = state.page; this.page = state.page;
this._monoMode = state.mono; this._monoMode = state.mono;
this._buffer[0] = base64_decode(state.buffer[0]); this._buffer[0] = new Uint8Array(state.buffer[0]);
this._buffer[1] = base64_decode(state.buffer[1]); this._buffer[1] = new Uint8Array(state.buffer[1]);
this.refresh(); this.refresh();
} }
@ -879,8 +878,8 @@ export class HiresPage2D implements HiresPage {
page: this.page, page: this.page,
mono: this._monoMode, mono: this._monoMode,
buffer: [ buffer: [
base64_encode(this._buffer[0]), new Uint8Array(this._buffer[0]),
base64_encode(this._buffer[1]) new Uint8Array(this._buffer[1]),
] ]
}; };
} }
@ -888,8 +887,8 @@ export class HiresPage2D implements HiresPage {
setState(state: GraphicsState) { setState(state: GraphicsState) {
this.page = state.page; this.page = state.page;
this._monoMode = state.mono; this._monoMode = state.mono;
this._buffer[0] = base64_decode(state.buffer[0]); this._buffer[0] = new Uint8Array(state.buffer[0]);
this._buffer[1] = base64_decode(state.buffer[1]); this._buffer[1] = new Uint8Array(state.buffer[1]);
this.refresh(); this.refresh();
} }

View File

@ -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. // Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary: function(drive, name, ext, rawData) { setBinary: function(drive, name, ext, rawData) {

View File

@ -9,7 +9,7 @@
* implied warranty. * 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 { bit, byte, DiskFormat, MemberOf, memory, nibble, rom } from '../types';
import { debug, toHex } from '../util'; import { debug, toHex } from '../util';
import { Disk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; import { Disk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
@ -151,6 +151,7 @@ type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
interface Callbacks { interface Callbacks {
driveLight: (drive: DriveNumber, on: boolean) => void; driveLight: (drive: DriveNumber, on: boolean) => void;
dirty: (drive: DriveNumber, dirty: boolean) => void; dirty: (drive: DriveNumber, dirty: boolean) => void;
label: (drive: DriveNumber, name: string) => void;
} }
/** Common information for Nibble and WOZ disks. */ /** Common information for Nibble and WOZ disks. */
@ -159,6 +160,8 @@ interface BaseDrive {
format: DiskFormat, format: DiskFormat,
/** Current disk volume number. */ /** Current disk volume number. */
volume: byte, volume: byte,
/** Displayed disk name */
name: string,
/** Quarter track position of read/write head. */ /** Quarter track position of read/write head. */
track: byte, track: byte,
/** Position of the head on the track. */ /** Position of the head on the track. */
@ -195,7 +198,8 @@ function isNibbleDrive(drive: Drive): drive is NibbleDrive {
interface DriveState { interface DriveState {
format: DiskFormat, format: DiskFormat,
volume: byte, volume: byte,
tracks: string[], name: string,
tracks: memory[],
track: byte, track: byte,
head: byte, head: byte,
phase: Phase, phase: Phase,
@ -217,7 +221,8 @@ function getDriveState(drive: Drive): DriveState {
const result: DriveState = { const result: DriveState = {
format: drive.format, format: drive.format,
volume: drive.volume, volume: drive.volume,
tracks: [] as string[], name: drive.name,
tracks: [],
track: drive.track, track: drive.track,
head: drive.head, head: drive.head,
phase: drive.phase, phase: drive.phase,
@ -228,7 +233,7 @@ function getDriveState(drive: Drive): DriveState {
throw Error('No tracks.'); throw Error('No tracks.');
} }
for (let idx = 0; idx < drive.tracks.length; idx++) { 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; return result;
} }
@ -238,6 +243,7 @@ function setDriveState(state: DriveState) {
const result: Drive = { const result: Drive = {
format: state.format, format: state.format,
volume: state.volume, volume: state.volume,
name: state.name,
tracks: [] as memory[], tracks: [] as memory[],
track: state.track, track: state.track,
head: state.head, head: state.head,
@ -246,8 +252,9 @@ function setDriveState(state: DriveState) {
dirty: state.dirty dirty: state.dirty
}; };
for (let idx = 0; idx < state.tracks.length; idx++) { 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; return result;
} }
@ -260,6 +267,7 @@ export default class DiskII {
{ // Drive 1 { // Drive 1
format: 'dsk', format: 'dsk',
volume: 254, volume: 254,
name: 'Disk 1',
tracks: [], tracks: [],
track: 0, track: 0,
head: 0, head: 0,
@ -270,6 +278,7 @@ export default class DiskII {
{ // Drive 2 { // Drive 2
format: 'dsk', format: 'dsk',
volume: 254, volume: 254,
name: 'Disk 2',
tracks: [], tracks: [],
track: 0, track: 0,
head: 0, head: 0,
@ -521,9 +530,7 @@ export default class DiskII {
this.offTimeout = window.setTimeout(() => { this.offTimeout = window.setTimeout(() => {
this.debug('Drive Off'); this.debug('Drive Off');
this.on = false; this.on = false;
if (this.callbacks.driveLight) { this.callbacks.driveLight(this.drive, false);
this.callbacks.driveLight(this.drive, false);
}
}, 1000); }, 1000);
} }
} }
@ -538,9 +545,7 @@ export default class DiskII {
this.debug('Drive On'); this.debug('Drive On');
this.on = true; this.on = true;
this.lastCycles = this.io.cycles(); this.lastCycles = this.io.cycles();
if (this.callbacks.driveLight) { this.callbacks.driveLight(this.drive, true);
this.callbacks.driveLight(this.drive, true);
}
} }
break; break;
@ -548,7 +553,7 @@ export default class DiskII {
this.debug('Disk 1'); this.debug('Disk 1');
this.drive = 1; this.drive = 1;
this.cur = this.drives[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(2, false);
this.callbacks.driveLight(1, true); this.callbacks.driveLight(1, true);
} }
@ -557,7 +562,7 @@ export default class DiskII {
this.debug('Disk 2'); this.debug('Disk 2');
this.drive = 2; this.drive = 2;
this.cur = this.drives[this.drive - 1]; this.cur = this.drives[this.drive - 1];
if (this.on && this.callbacks.driveLight) { if (this.on) {
this.callbacks.driveLight(1, false); this.callbacks.driveLight(1, false);
this.callbacks.driveLight(2, true); this.callbacks.driveLight(2, true);
} }
@ -685,9 +690,11 @@ export default class DiskII {
this.on = state.on; this.on = state.on;
this.drive = state.drive; this.drive = state.drive;
for (const d of DRIVE_NUMBERS) { 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.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]; this.cur = this.drives[this.drive - 1];
} }
@ -777,6 +784,7 @@ export default class DiskII {
Object.assign(cur, newDisk); Object.assign(cur, newDisk);
this.updateDirty(this.drive, false); this.updateDirty(this.drive, false);
this.callbacks.label(this.drive, name);
} }
getJSON(drive: DriveNumber, pretty: boolean) { getJSON(drive: DriveNumber, pretty: boolean) {
@ -831,6 +839,7 @@ export default class DiskII {
Object.assign(cur, disk); Object.assign(cur, disk);
this.updateDirty(drive, true); this.updateDirty(drive, true);
this.callbacks.label(this.drive, name);
return true; return true;
} }

View File

@ -115,5 +115,12 @@ export default function NoSlotClock(rom)
_access(off); _access(off);
rom.write(page, off, val); rom.write(page, off, val);
}, },
getState() {
return {};
},
setState(_) {
}
}; };
} }

View File

@ -39,6 +39,10 @@ export default function Parallel(io, cbs) {
read: function(page, off) { read: function(page, off) {
return rom[off]; return rom[off];
}, },
write: function() {} write: function() {},
getState() {
return {};
},
setState(_) {}
}; };
} }

View File

@ -9,7 +9,6 @@
* implied warranty. * implied warranty.
*/ */
import { base64_decode, base64_encode } from '../base64';
import { allocMem, debug } from '../util'; import { allocMem, debug } from '../util';
import { rom } from '../roms/cards/ramfactor'; import { rom } from '../roms/cards/ramfactor';
@ -140,14 +139,14 @@ export default function RAMFactor(io, size) {
return { return {
loc: _loc, loc: _loc,
firmware: _firmware, firmware: _firmware,
mem: base64_encode(mem) mem: new Uint8Array(mem)
}; };
}, },
setState: function(state) { setState: function(state) {
_loc = state.loc; _loc = state.loc;
_firmware = state.firmware; _firmware = state.firmware;
mem = base64_decode(state.mem); mem = new Uint8Array(state.mem);
_ramhi = (_loc >> 16) & 0xff; _ramhi = (_loc >> 16) & 0xff;
_rammid = (_loc >> 8) & 0xff; _rammid = (_loc >> 8) & 0xff;

View File

@ -9,7 +9,6 @@
* implied warranty. * implied warranty.
*/ */
import { base64_decode } from '../base64';
import { debug, toHex } from '../util'; import { debug, toHex } from '../util';
import { rom } from '../roms/cards/smartport'; import { rom } from '../roms/cards/smartport';
@ -21,7 +20,6 @@ export default function SmartPort(io, cpu, options ) {
var BLOCK_LO = 0x46; var BLOCK_LO = 0x46;
// var BLOCK_HI = 0x47; // var BLOCK_HI = 0x47;
var disks = []; var disks = [];
function _init() { function _init() {
@ -36,7 +34,7 @@ export default function SmartPort(io, cpu, options ) {
function decodeDisk(unit, disk) { function decodeDisk(unit, disk) {
disks[unit] = []; disks[unit] = [];
for (var idx = 0; idx < disk.blocks.length; idx++) { 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() { 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) { setBinary: function (drive, name, fmt, data) {

View File

@ -150,6 +150,10 @@ export default function Thunderclock()
}, },
ioSwitch: function thunderclock_ioSwitch(off, val) { ioSwitch: function thunderclock_ioSwitch(off, val) {
return _access(off, val); return _access(off, val);
} },
getState() {
return {};
},
setState(_) {}
}; };
} }

View File

@ -256,6 +256,29 @@ export default function Videoterm(_io) {
return _imageData; return _imageData;
} }
return; 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;
} }
}; };
} }

View File

@ -9,7 +9,6 @@
* implied warranty. * implied warranty.
*/ */
import { base64_decode, base64_encode } from './base64';
import { byte, memory, Memory, Restorable } from './types'; import { byte, memory, Memory, Restorable } from './types';
import { allocMemPages } from './util'; import { allocMemPages } from './util';
@ -340,16 +339,16 @@ export class LoresPageGL implements LoresPage {
page: this.page, page: this.page,
mono: this._monoMode, mono: this._monoMode,
buffer: [ buffer: [
base64_encode(this._buffer[0]), new Uint8Array(this._buffer[0]),
base64_encode(this._buffer[1]) new Uint8Array(this._buffer[1]),
] ]
}; };
} }
setState(state: GraphicsState) { setState(state: GraphicsState) {
this.page = state.page; this.page = state.page;
this._buffer[0] = base64_decode(state.buffer[0]); this._buffer[0] = new Uint8Array(state.buffer[0]);
this._buffer[1] = base64_decode(state.buffer[1]); this._buffer[1] = new Uint8Array(state.buffer[1]);
this.refresh(); this.refresh();
} }
@ -646,16 +645,16 @@ export class HiresPageGL implements Memory, Restorable<GraphicsState> {
page: this.page, page: this.page,
mono: this._monoMode, mono: this._monoMode,
buffer: [ buffer: [
base64_encode(this._buffer[0]), new Uint8Array(this._buffer[0]),
base64_encode(this._buffer[1]) new Uint8Array(this._buffer[1]),
] ]
}; };
} }
setState(state: GraphicsState) { setState(state: GraphicsState) {
this.page = state.page; this.page = state.page;
this._buffer[0] = base64_decode(state.buffer[0]); this._buffer[0] = new Uint8Array(state.buffer[0]);
this._buffer[1] = base64_decode(state.buffer[1]); this._buffer[1] = new Uint8Array(state.buffer[1]);
this.refresh(); this.refresh();
} }
@ -914,6 +913,7 @@ export class VideoModesGL implements VideoModes {
this._grs[1].setState(state.grs[1]); this._grs[1].setState(state.grs[1]);
this._hgrs[0].setState(state.hgrs[0]); this._hgrs[0].setState(state.hgrs[0]);
this._hgrs[1].setState(state.hgrs[1]); this._hgrs[1].setState(state.hgrs[1]);
this._refresh();
} }
mono(on: boolean) { mono(on: boolean) {

105
js/mmu.ts
View File

@ -10,9 +10,10 @@
*/ */
import CPU6502 from './cpu6502'; 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 { debug, toHex } from './util';
import { byte, Memory } from './types'; import { byte, Memory, Restorable } from './types';
import Apple2IO from './apple2io'; import Apple2IO from './apple2io';
import { HiresPage, LoresPage, VideoModes } from './videomodes'; import { HiresPage, LoresPage, VideoModes } from './videomodes';
@ -138,10 +139,18 @@ class Switches implements Memory {
} }
} }
class AuxRom { class AuxRom implements Memory {
constructor( constructor(
private readonly mmu: MMU, private readonly mmu: MMU,
private readonly rom: Memory) { } private readonly rom: ROM) { }
start() {
return 0xc1;
}
end() {
return 0xcf;
}
read(page: byte, off: byte) { read(page: byte, off: byte) {
if (page == 0xc3) { if (page == 0xc3) {
@ -160,38 +169,33 @@ class AuxRom {
} }
} }
/* export interface MMUState {
interface State { bank1: boolean
bank1: this._bank1, readbsr: boolean
readbsr: this._readbsr, writebsr: boolean
writebsr: this._writebsr, prewrite: boolean
prewrite: this._prewrite,
intcxrom: this._intcxrom, intcxrom: boolean
slot3rom: this._slot3rom, slot3rom: boolean
intc8rom: this._intc8rom, intc8rom: boolean
auxRamRead: this._auxRamRead, auxRamRead: boolean
auxRamWrite: this._auxRamWrite, auxRamWrite: boolean
altzp: this._altzp, altzp: boolean
_80store: this._80store, _80store: boolean
page2: this._page2, page2: boolean
hires: this._hires, hires: boolean
mem00_01: [this.mem00_01[0].getState(), this.mem00_01[1].getState()], mem00_01: [RAMState, RAMState]
mem02_03: [this.mem02_03[0].getState(), this.mem02_03[1].getState()], mem02_03: [RAMState, RAMState]
mem0C_1F: [this.mem0C_1F[0].getState(), this.mem0C_1F[1].getState()], mem0C_1F: [RAMState, RAMState]
mem60_BF: [this.mem60_BF[0].getState(), this.mem60_BF[1].getState()], mem60_BF: [RAMState, RAMState]
memD0_DF: [ memD0_DF: [ROMState, RAMState, RAMState, RAMState, RAMState]
this.memD0_DF[0].getState(), this.memD0_DF[1].getState(), memE0_FF: [ROMState, RAMState, RAMState]
this.memD0_DF[2].getState(), this.memD0_DF[3].getState() }
],
memE0_FF: [this.memE0_FF[0].getState(), this.memE0_FF[1].getState()]
};
*/
export default class MMU implements Memory { export default class MMU implements Memory, Restorable<MMUState> {
private _readPages = new Array(0x100); private _readPages = new Array(0x100);
private _writePages = new Array(0x100); private _writePages = new Array(0x100);
private _pages = 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 mem60_BF = [new RAM(0x60, 0xBF), new RAM(0x60, 0xBF)];
private memC0_C0 = [this.switches]; private memC0_C0 = [this.switches];
private memC1_CF = [this.io, this.auxRom]; private memC1_CF = [this.io, this.auxRom];
private memD0_DF = [ private memD0_DF: [ROM, RAM, RAM, RAM, RAM] = [
this.rom, this.rom,
new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF), new RAM(0xD0, 0xDF),
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( constructor(
private readonly cpu: CPU6502, private readonly cpu: CPU6502,
@ -250,8 +257,7 @@ export default class MMU implements Memory {
private readonly hires1: HiresPage, private readonly hires1: HiresPage,
private readonly hires2: HiresPage, private readonly hires2: HiresPage,
private readonly io: Apple2IO, private readonly io: Apple2IO,
// TODO(flan): Better typing. private readonly rom: ROM) {
private readonly rom: any) {
/* /*
* Initialize read/write banks * Initialize read/write banks
*/ */
@ -332,7 +338,7 @@ export default class MMU implements Memory {
} }
_initSwitches() { _initSwitches() {
this._bank1 = true; this._bank1 = false;
this._readbsr = false; this._readbsr = false;
this._writebsr = false; this._writebsr = false;
this._prewrite = false; this._prewrite = false;
@ -352,7 +358,7 @@ export default class MMU implements Memory {
this._iouDisable = true; this._iouDisable = true;
} }
_debug(..._args: any) { _debug(..._args: any[]) {
// debug.apply(this, arguments); // debug.apply(this, arguments);
} }
@ -813,6 +819,9 @@ export default class MMU implements Memory {
} }
public read(page: byte, off: byte) { public read(page: byte, off: byte) {
if (page === 0xff && off === 0xfc && this._intcxrom) {
this._initSwitches();
}
return this._readPages[page].read(page, off); return this._readPages[page].read(page, off);
} }
@ -823,8 +832,8 @@ export default class MMU implements Memory {
public resetVB() { public resetVB() {
this._vbEnd = this.cpu.getCycles() + 1000; this._vbEnd = this.cpu.getCycles() + 1000;
} }
/*
public getState(): State { public getState(): MMUState {
return { return {
bank1: this._bank1, bank1: this._bank1,
readbsr: this._readbsr, 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()], mem0C_1F: [this.mem0C_1F[0].getState(), this.mem0C_1F[1].getState()],
mem60_BF: [this.mem60_BF[0].getState(), this.mem60_BF[1].getState()], mem60_BF: [this.mem60_BF[0].getState(), this.mem60_BF[1].getState()],
memD0_DF: [ memD0_DF: [
this.memD0_DF[0].getState(), this.memD0_DF[1].getState(), this.memD0_DF[0].getState(),
this.memD0_DF[2].getState(), this.memD0_DF[3].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._readbsr = state.readbsr;
this._writebsr = state.writebsr; this._writebsr = state.writebsr;
this._bank1 = state.bank1; 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[1].setState(state.memD0_DF[1]);
this.memD0_DF[2].setState(state.memD0_DF[2]); this.memD0_DF[2].setState(state.memD0_DF[2]);
this.memD0_DF[3].setState(state.memD0_DF[3]); 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[0].setState(state.memE0_FF[0]);
this.memE0_FF[1].setState(state.memE0_FF[1]); this.memE0_FF[1].setState(state.memE0_FF[1]);
this.memE0_FF[2].setState(state.memE0_FF[2]);
this._updateBanks(); this._updateBanks();
} }
*/
} }

View File

@ -9,24 +9,19 @@
* implied warranty. * implied warranty.
*/ */
import { base64_decode, base64_encode } from './base64'; import { byte, memory, Memory, Restorable } from './types';
import { byte, memory, Memory } from './types';
import { allocMemPages } from './util'; import { allocMemPages } from './util';
export interface State { export interface RAMState {
/** Start of memory region. */ /** Copy of contents. */
start: byte; mem: memory;
/** End of memory region. */
end: byte;
/** Base64-encoded contents. */
mem: string;
} }
/** /**
* Represents RAM from the start page `sp` to end page `ep`. The memory * Represents RAM from the start page `sp` to end page `ep`. The memory
* is addressed by `page` and `offset`. * is addressed by `page` and `offset`.
*/ */
export default class RAM implements Memory { export default class RAM implements Memory, Restorable<RAMState> {
private start_page: byte; private start_page: byte;
private end_page: byte; private end_page: byte;
private mem: memory; private mem: memory;
@ -54,17 +49,13 @@ export default class RAM implements Memory {
this.mem[(page - this.start_page) << 8 | offset] = val; this.mem[(page - this.start_page) << 8 | offset] = val;
} }
public getState(): State { public getState(): RAMState {
return { return {
start: this.start_page, mem: new Uint8Array(this.mem)
end: this.end_page,
mem: base64_encode(this.mem)
}; };
} }
public setState(state: State) { public setState(state: RAMState) {
this.start_page = state.start; this.mem = new Uint8Array(state.mem);
this.end_page = state.end;
this.mem = base64_decode(state.mem);
} }
} }

View File

@ -1,7 +1,9 @@
import { PageHandler } from '../cpu6502'; 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<ROMState> {
constructor( constructor(
private readonly startPage: byte, private readonly startPage: byte,
@ -24,4 +26,9 @@ export default class ROM implements PageHandler {
} }
write() { write() {
} }
getState() {
return null;
}
setState(_state: null) {
}
} }

View File

@ -42,7 +42,7 @@ export interface Memory {
} }
/* An interface card */ /* An interface card */
export interface Card extends Memory { export interface Card extends Memory, Restorable {
/* Reset the card */ /* Reset the card */
reset(): void; reset(): void;
@ -87,7 +87,7 @@ export interface DiskIIDrive extends Drive {
export type TapeData = Array<[duration: number, high: boolean]>; export type TapeData = Array<[duration: number, high: boolean]>;
export interface Restorable<T> { export interface Restorable<T = any> {
getState(): T; getState(): T;
setState(state: T): void; setState(state: T): void;
} }

View File

@ -1,5 +1,6 @@
import MicroModal from 'micromodal'; import MicroModal from 'micromodal';
import { base64_json_parse, base64_json_stringify } from '../base64';
import Audio from './audio'; import Audio from './audio';
import DriveLights from './drive_lights'; import DriveLights from './drive_lights';
import { DISK_FORMATS } from '../types'; import { DISK_FORMATS } from '../types';
@ -301,13 +302,11 @@ function doLoadLocalDisk(drive, file) {
if (this.result.byteLength >= 800 * 1024) { if (this.result.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, this.result)) { if (_smartPort.setBinary(drive, name, ext, this.result)) {
driveLights.label(drive, name);
focused = false; focused = false;
initGamepad(); initGamepad();
} }
} else { } else {
if (_disk2.setBinary(drive, name, ext, this.result)) { if (_disk2.setBinary(drive, name, ext, this.result)) {
driveLights.label(drive, name);
focused = false; focused = false;
initGamepad(); initGamepad();
} }
@ -362,12 +361,10 @@ export function doLoadHTTP(drive, _url) {
var name = decodeURIComponent(fileParts.join('.')); var name = decodeURIComponent(fileParts.join('.'));
if (data.byteLength >= 800 * 1024) { if (data.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, data)) { if (_smartPort.setBinary(drive, name, ext, data)) {
driveLights.label(drive, name);
initGamepad(); initGamepad();
} }
} else { } else {
if (_disk2.setBinary(drive, name, ext, data)) { if (_disk2.setBinary(drive, name, ext, data)) {
driveLights.label(drive, name);
initGamepad(); initGamepad();
} }
} }
@ -503,7 +500,6 @@ function loadDisk(drive, disk) {
disk_cur_cat[drive] = category; disk_cur_cat[drive] = category;
disk_cur_name[drive] = name; disk_cur_name[drive] = name;
driveLights.label(drive, name);
_disk2.setDisk(drive, disk); _disk2.setDisk(drive, disk);
initGamepad(disk.gamepad); initGamepad(disk.gamepad);
} }
@ -635,7 +631,7 @@ function _keydown(evt) {
evt.preventDefault(); evt.preventDefault();
var key = keyboard.mapKeyEvent(evt); var key = keyboard.mapKeyEvent(evt);
if (key != 0xff) { if (key !== 0xff) {
io.keyDown(key); io.keyDown(key);
} }
} }
@ -666,9 +662,11 @@ function _keydown(evt) {
} else if (evt.keyCode === 114) { // F3 } else if (evt.keyCode === 114) { // F3
io.keyDown(0x1b); io.keyDown(0x1b);
} else if (evt.keyCode === 117) { // F6 Quick Save } 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 } 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 } else if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(true); keyboard.shiftKey(true);
} else if (evt.keyCode == 20) { // Caps lock } else if (evt.keyCode == 20) { // Caps lock

View File

@ -15,20 +15,6 @@ export default function DriveLights()
document.querySelector('#disk-label' + drive).innerText = label; document.querySelector('#disk-label' + drive).innerText = label;
} }
return document.querySelector('#disk-label' + drive).innerText; 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);
}
} }
}; };
} }

View File

@ -210,7 +210,7 @@ export default function KeyBoard(cpu, io, e) {
return { return {
mapKeyEvent: function keyboard_mapKeyEvent(evt) { mapKeyEvent: function keyboard_mapKeyEvent(evt) {
var code = evt.keyCode, key = '\xff'; var code = evt.keyCode, key = 0xff;
if (evt.key in uiKitMap) { if (evt.key in uiKitMap) {
key = uiKitMap[evt.key]; key = uiKitMap[evt.key];
@ -230,7 +230,7 @@ export default function KeyBoard(cpu, io, e) {
if (key == 0x7F && evt.shiftKey && evt.ctrlKey) { if (key == 0x7F && evt.shiftKey && evt.ctrlKey) {
cpu.reset(); cpu.reset();
key = '\xff'; key = 0xff;
} }
return key; return key;

View File

@ -1,4 +1,4 @@
import { Memory, Restorable, byte } from './types'; import { Memory, Restorable, byte, memory } from './types';
export type bank = 0 | 1; export type bank = 0 | 1;
export type pageNo = 1 | 2; export type pageNo = 1 | 2;
@ -19,7 +19,7 @@ export interface Region {
export interface GraphicsState { export interface GraphicsState {
page: byte; page: byte;
mono: boolean; mono: boolean;
buffer: string[]; buffer: memory[];
} }
export interface VideoModesState { export interface VideoModesState {

60
test/js/base64.test.ts Normal file
View File

@ -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"}}'
);
});
});
});