mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
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:
parent
983026aa1d
commit
afc5280ac2
49
js/apple2.ts
49
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<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() {
|
||||
|
@ -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?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
24
js/base64.ts
24
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);
|
||||
}
|
||||
|
17
js/canvas.ts
17
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();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,5 +115,12 @@ export default function NoSlotClock(rom)
|
||||
_access(off);
|
||||
rom.write(page, off, val);
|
||||
},
|
||||
|
||||
getState() {
|
||||
return {};
|
||||
},
|
||||
|
||||
setState(_) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ export default function Parallel(io, cbs) {
|
||||
read: function(page, off) {
|
||||
return rom[off];
|
||||
},
|
||||
write: function() {}
|
||||
write: function() {},
|
||||
getState() {
|
||||
return {};
|
||||
},
|
||||
setState(_) {}
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -150,6 +150,10 @@ export default function Thunderclock()
|
||||
},
|
||||
ioSwitch: function thunderclock_ioSwitch(off, val) {
|
||||
return _access(off, val);
|
||||
}
|
||||
},
|
||||
getState() {
|
||||
return {};
|
||||
},
|
||||
setState(_) {}
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
18
js/gl.ts
18
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<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
105
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<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();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
27
js/ram.ts
27
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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
60
test/js/base64.test.ts
Normal 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"}}'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user