apple2js/js/apple2.ts
Ian Flanigan 04ae0327c2
Add the recommended eslint plugins for TypeScript (#121)
This adds both the recommended TypeScript checks, plus the recommended
TypeScript checks that require type checking.  This latter addition
means that eslint essentially has to compile all of the TypeScript in
the project, causing it to be slower. This isn't much of a problem in
VS Code because there's a lot of caching being done, but it's clearly
slower when run on the commandline.

All of the errors are either fixed or suppressed.  Some errors are
suppressed because fixing them would be too laborious for the little
value gained.

The eslint config is also slightly refactored to separate the strictly
TypeScript checks from the JavaScript checks.
2022-05-31 08:38:40 -07:00

280 lines
7.3 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 { ReadonlyUint8Array, 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;
}
export 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}`) as Promise<{ default: new () => ROM }>;
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>;
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;
}
}