mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
d44cae76a7
Turn on exactOptionalPropertyTypes
280 lines
7.2 KiB
TypeScript
280 lines
7.2 KiB
TypeScript
import Apple2IO from './apple2io';
|
|
import {
|
|
HiresPage,
|
|
LoresPage,
|
|
VideoModes,
|
|
VideoModesState,
|
|
} from './videomodes';
|
|
import {
|
|
HiresPage2D,
|
|
LoresPage2D,
|
|
VideoModes2D,
|
|
} from './canvas';
|
|
import {
|
|
HiresPageGL,
|
|
LoresPageGL,
|
|
VideoModesGL,
|
|
} from './gl';
|
|
import ROM from './roms/rom';
|
|
import { Apple2IOState } from './apple2io';
|
|
import CPU6502, {
|
|
CpuState,
|
|
FLAVOR_6502,
|
|
FLAVOR_ROCKWELL_65C02,
|
|
} from './cpu6502';
|
|
import MMU, { MMUState } from './mmu';
|
|
import RAM, { RAMState } from './ram';
|
|
|
|
import SYMBOLS from './symbols';
|
|
import Debugger, { DebuggerContainer } from './debugger';
|
|
|
|
import { Restorable, rom } from './types';
|
|
import { processGamepad } from './ui/gamepad';
|
|
|
|
export interface Apple2Options {
|
|
characterRom: string;
|
|
enhanced: boolean;
|
|
e: boolean;
|
|
gl: boolean;
|
|
rom: string;
|
|
canvas: HTMLCanvasElement;
|
|
tick: () => void;
|
|
}
|
|
|
|
export interface Stats {
|
|
cycles: number;
|
|
frames: number;
|
|
renderedFrames: number;
|
|
}
|
|
|
|
interface State {
|
|
cpu: CpuState;
|
|
vm: VideoModesState;
|
|
io: Apple2IOState;
|
|
mmu: MMUState | undefined;
|
|
ram: RAMState[] | undefined;
|
|
}
|
|
|
|
export class Apple2 implements Restorable<State>, DebuggerContainer {
|
|
private paused = false;
|
|
|
|
private theDebugger: Debugger | undefined;
|
|
|
|
private runTimer: number | null = null;
|
|
private runAnimationFrame: number | null = null;
|
|
private cpu: CPU6502;
|
|
|
|
private gr: LoresPage;
|
|
private gr2: LoresPage;
|
|
private hgr: HiresPage;
|
|
private hgr2: HiresPage;
|
|
private vm: VideoModes;
|
|
|
|
private io: Apple2IO;
|
|
private mmu: MMU | undefined;
|
|
private ram: [RAM, RAM, RAM] | undefined;
|
|
private characterRom: rom;
|
|
private rom: ROM;
|
|
|
|
private tick: () => void;
|
|
|
|
private stats: Stats = {
|
|
cycles: 0,
|
|
frames: 0,
|
|
renderedFrames: 0
|
|
};
|
|
|
|
public ready: Promise<void>;
|
|
|
|
constructor(options: Apple2Options) {
|
|
this.ready = this.init(options);
|
|
}
|
|
|
|
async init(options: Apple2Options) {
|
|
const romImportPromise = import(`./roms/system/${options.rom}`);
|
|
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`);
|
|
|
|
const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
|
|
const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
|
|
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
|
|
|
|
this.cpu = new CPU6502({
|
|
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502
|
|
});
|
|
this.vm = new VideoModes(options.canvas, options.e);
|
|
|
|
const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([
|
|
romImportPromise,
|
|
characterRomImportPromise,
|
|
this.vm.ready,
|
|
]);
|
|
|
|
this.rom = new Apple2ROM();
|
|
this.characterRom = characterRom;
|
|
|
|
this.gr = new LoresPage(this.vm, 1, this.characterRom, options.e);
|
|
this.gr2 = new LoresPage(this.vm, 2, this.characterRom, options.e);
|
|
this.hgr = new HiresPage(this.vm, 1);
|
|
this.hgr2 = new HiresPage(this.vm, 2);
|
|
this.io = new Apple2IO(this.cpu, this.vm);
|
|
this.tick = options.tick;
|
|
|
|
if (options.e) {
|
|
this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, this.rom);
|
|
this.cpu.addPageHandler(this.mmu);
|
|
} else {
|
|
this.ram = [
|
|
new RAM(0x00, 0x03),
|
|
new RAM(0x0C, 0x1F),
|
|
new RAM(0x60, 0xBF)
|
|
];
|
|
|
|
this.cpu.addPageHandler(this.ram[0]);
|
|
this.cpu.addPageHandler(this.gr);
|
|
this.cpu.addPageHandler(this.gr2);
|
|
this.cpu.addPageHandler(this.ram[1]);
|
|
this.cpu.addPageHandler(this.hgr);
|
|
this.cpu.addPageHandler(this.hgr2);
|
|
this.cpu.addPageHandler(this.ram[2]);
|
|
this.cpu.addPageHandler(this.io);
|
|
this.cpu.addPageHandler(this.rom);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs the emulator. If the emulator is already running, this does
|
|
* nothing. When this function exits either `runTimer` or
|
|
* `runAnimationFrame` will be non-null.
|
|
*/
|
|
run() {
|
|
this.paused = false;
|
|
if (this.runTimer || this.runAnimationFrame) {
|
|
return; // already running
|
|
}
|
|
|
|
this.theDebugger = new Debugger(this);
|
|
this.theDebugger.addSymbols(SYMBOLS);
|
|
|
|
const interval = 30;
|
|
|
|
let now, last = Date.now();
|
|
const runFn = () => {
|
|
const kHz = this.io.getKHz();
|
|
now = Date.now();
|
|
|
|
const stepMax = kHz * interval;
|
|
let step = (now - last) * kHz;
|
|
last = now;
|
|
if (step > stepMax) {
|
|
step = stepMax;
|
|
}
|
|
|
|
if (this.theDebugger) {
|
|
this.theDebugger.stepCycles(step);
|
|
} else {
|
|
this.cpu.stepCycles(step);
|
|
}
|
|
if (this.mmu) {
|
|
this.mmu.resetVB();
|
|
}
|
|
if (this.io.annunciator(0)) {
|
|
const imageData = this.io.blit();
|
|
if (imageData) {
|
|
this.vm.blit(imageData);
|
|
this.stats.renderedFrames++;
|
|
}
|
|
} else {
|
|
if (this.vm.blit()) {
|
|
this.stats.renderedFrames++;
|
|
}
|
|
}
|
|
this.stats.cycles = this.cpu.getCycles();
|
|
this.stats.frames++;
|
|
this.io.tick();
|
|
this.tick();
|
|
processGamepad(this.io);
|
|
|
|
if (!this.paused && requestAnimationFrame) {
|
|
this.runAnimationFrame = requestAnimationFrame(runFn);
|
|
}
|
|
};
|
|
if (requestAnimationFrame) {
|
|
this.runAnimationFrame = requestAnimationFrame(runFn);
|
|
} else {
|
|
this.runTimer = window.setInterval(runFn, interval);
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
this.paused = true;
|
|
if (this.runTimer) {
|
|
clearInterval(this.runTimer);
|
|
}
|
|
if (this.runAnimationFrame) {
|
|
cancelAnimationFrame(this.runAnimationFrame);
|
|
}
|
|
this.runTimer = null;
|
|
this.runAnimationFrame = null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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() {
|
|
this.cpu.reset();
|
|
}
|
|
|
|
getStats(): Stats {
|
|
return this.stats;
|
|
}
|
|
|
|
getCPU() {
|
|
return this.cpu;
|
|
}
|
|
|
|
getIO() {
|
|
return this.io;
|
|
}
|
|
|
|
getMMU() {
|
|
return this.mmu;
|
|
}
|
|
|
|
getROM() {
|
|
return this.rom;
|
|
}
|
|
|
|
getVideoModes() {
|
|
return this.vm;
|
|
}
|
|
|
|
getDebugger() {
|
|
return this.theDebugger;
|
|
}
|
|
}
|