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 * 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<State> {
@ -57,7 +63,8 @@ export class Apple2 implements Restorable<State> {
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<State> {
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<State> {
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<State> {
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() {

View File

@ -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<any | null>
}
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<Apple2IOState> {
private _slot: Array<Card | null> = 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?.();
}
}

View File

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

View File

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

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.
setBinary: function(drive, name, ext, rawData) {

View File

@ -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<typeof DRIVE_NUMBERS>;
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;
}
}
}

View File

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

View File

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

View File

@ -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;

View File

@ -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) {

View File

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

View File

@ -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;
}
};
}

View File

@ -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<GraphicsState> {
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) {

105
js/mmu.ts
View File

@ -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<MMUState> {
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();
}
*/
}

View File

@ -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<RAMState> {
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);
}
}

View File

@ -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<ROMState> {
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) {
}
}

View File

@ -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<T> {
export interface Restorable<T = any> {
getState(): T;
setState(state: T): void;
}

View File

@ -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

View File

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

View File

@ -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;

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 pageNo = 1 | 2;
@ -19,7 +19,7 @@ export interface Region {
export interface GraphicsState {
page: byte;
mono: boolean;
buffer: string[];
buffer: memory[];
}
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"}}'
);
});
});
});