Lazy load ROMs (#81)

* Switch modules to `esnext` to allow `webpack` to see import statements
* Pass rom names into Apple2 class
* Move ROMs into `system` and `character` directories to allow webpack bundle appropriate ROMs.
* Wait for ROMs to load before completing initialization.
This commit is contained in:
Will Scullin 2021-06-13 17:06:16 -07:00 committed by GitHub
parent af57378852
commit 8087294456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 199 additions and 162 deletions

View File

@ -25,6 +25,7 @@
"always"
],
"no-use-before-define": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-use-before-define": [
"error",
{
@ -59,7 +60,8 @@
"es6": true
},
"parserOptions": {
"sourceType": "module"
"sourceType": "module",
"project": "./tsconfig.json"
},
"extends": "eslint:recommended",
"overrides": [
@ -109,5 +111,6 @@
"commonjs": true
}
}
]
],
"ignorePatterns": ["coverage/**/*"]
}

View File

@ -309,6 +309,6 @@
</filter>
</svg>
<script src="dist/main2.js"></script>
<script src="dist/main2.bundle.js"></script>
</body>
</html>

View File

@ -29,10 +29,6 @@
<link rel="stylesheet" type="text/css" href="css/apple2.css" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.2/css/all.css" />
<script>
window.e = true
</script>
<!-- Disk Index -->
<script type="text/javascript" src="json/disks/index.js"></script>
@ -318,6 +314,6 @@
</filter>
</svg>
<script src="dist/main2e.js"></script>
<script src="dist/main2e.bundle.js"></script>
</body>
</html>

View File

@ -28,11 +28,11 @@ import { Restorable, rom } from './types';
import { processGamepad } from './ui/gamepad';
export interface Apple2Options {
characterRom: rom,
characterRom: string;
enhanced: boolean,
e: boolean,
gl: boolean,
rom: ROM,
rom: string,
canvas: HTMLCanvasElement,
tick: () => void,
}
@ -68,6 +68,8 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
private io: Apple2IO;
private mmu: MMU | undefined;
private ram: [RAM, RAM, RAM] | undefined;
private characterRom: rom;
private rom: ROM;
private tick: () => void;
@ -76,22 +78,41 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
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({ '65C02': options.enhanced });
this.vm = new VideoModes(options.canvas, options.e);
this.gr = new LoresPage(this.vm, 1, options.characterRom, options.e);
this.gr2 = new LoresPage(this.vm, 2, options.characterRom, 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, options.rom);
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 = [
@ -108,7 +129,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.cpu.addPageHandler(this.hgr2);
this.cpu.addPageHandler(this.ram[2]);
this.cpu.addPageHandler(this.io);
this.cpu.addPageHandler(options.rom);
this.cpu.addPageHandler(this.rom);
}
}
@ -235,6 +256,9 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
return this.mmu;
}
getROM() {
return this.rom;
}
getVideoModes() {
return this.vm;

View File

@ -11,49 +11,43 @@ import SmartPort from './cards/smartport';
import Thunderclock from './cards/thunderclock';
import VideoTerm from './cards/videoterm';
import apple2_charset from './roms/apple2_char';
import apple2j_charset from './roms/apple2j_char';
import apple2lc_charset from './roms/apple2lc_char';
import pigfont_charset from './roms/pigfont_char';
import Apple2ROM from './roms/fpbasic';
import Apple2jROM from './roms/apple2j';
import IntBASIC from './roms/intbasic';
import OriginalROM from './roms/original';
import { Apple2 } from './apple2';
const prefs = new Prefs();
const romVersion = prefs.readPref('computer_type2');
let rom;
let characterRom = apple2_charset;
let rom: string;
let characterRom: string;
let sectors = 16;
switch (romVersion) {
case 'apple2':
rom = new IntBASIC();
rom = 'intbasic';
characterRom = 'apple2_char';
break;
case 'apple213':
rom = new IntBASIC();
rom = 'intbasic';
characterRom = 'apple2_char';
sectors = 13;
break;
case 'original':
rom = new OriginalROM();
rom = 'original';
characterRom = 'apple2_char';
break;
case 'apple2jplus':
rom = new Apple2jROM();
characterRom = apple2j_charset;
rom = 'apple2j';
characterRom = 'apple2j_char';
break;
case 'apple2pig':
rom = new Apple2ROM();
characterRom = pigfont_charset;
rom = 'fpbasic';
characterRom = 'pigfont_char';
break;
case 'apple2lc':
rom = new Apple2ROM();
characterRom = apple2lc_charset;
rom = 'fpbasic';
characterRom = 'apple2lc_char';
break;
default:
rom = new Apple2ROM();
rom = 'fpbasic';
characterRom = 'apple2_char';
}
const options = {
@ -67,27 +61,29 @@ const options = {
};
export const apple2 = new Apple2(options);
const cpu = apple2.getCPU();
const io = apple2.getIO();
apple2.ready.then(() => {
const cpu = apple2.getCPU();
const io = apple2.getIO();
const printer = new Printer('#printer-modal .paper');
const printer = new Printer('#printer-modal .paper');
const lc = new LanguageCard(rom);
const parallel = new Parallel(printer);
const videoTerm = new VideoTerm();
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights, sectors);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, { block: true });
const lc = new LanguageCard(apple2.getROM());
const parallel = new Parallel(printer);
const videoTerm = new VideoTerm();
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights, sectors);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, { block: true });
io.setSlot(0, lc);
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(4, clock);
io.setSlot(3, videoTerm);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
io.setSlot(0, lc);
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(4, clock);
io.setSlot(3, videoTerm);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
cpu.addPageHandler(lc);
cpu.addPageHandler(lc);
initUI(apple2, disk2, smartport, printer, false);
initUI(apple2, disk2, smartport, printer, false);
}).catch(console.error);

View File

@ -9,33 +9,27 @@ import RAMFactor from './cards/ramfactor';
import SmartPort from './cards/smartport';
import Thunderclock from './cards/thunderclock';
import apple2e_charset from './roms/apple2e_char';
import apple2enh_charset from './roms/apple2enh_char';
import rmfont_charset from './roms/rmfont_char';
import Apple2eROM from './roms/apple2e';
import Apple2eEnhancedROM from './roms/apple2enh';
import { Apple2 } from './apple2';
const prefs = new Prefs();
const romVersion = prefs.readPref('computer_type2e');
let enhanced = false;
let rom;
let characterRom = apple2e_charset;
let rom: string;
let characterRom: string;
switch (romVersion) {
case 'apple2e':
rom = new Apple2eROM();
rom = 'apple2e';
characterRom = 'apple2e_char';
break;
case 'apple2rm':
rom = new Apple2eEnhancedROM();
characterRom = rmfont_charset;
rom = 'apple2e';
characterRom = 'rmfont_char';
enhanced = true;
break;
default:
rom = new Apple2eEnhancedROM();
characterRom = apple2enh_charset;
rom = 'apple2enh';
characterRom = 'apple2enh_char';
enhanced = true;
}
@ -50,21 +44,23 @@ const options = {
};
export const apple2 = new Apple2(options);
const io = apple2.getIO();
const cpu = apple2.getCPU();
apple2.ready.then(() => {
const io = apple2.getIO();
const cpu = apple2.getCPU();
const printer = new Printer('#printer-modal .paper');
const printer = new Printer('#printer-modal .paper');
const parallel = new Parallel(printer);
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, { block: !enhanced });
const parallel = new Parallel(printer);
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, { block: !enhanced });
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(5, clock);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(5, clock);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
initUI(apple2, disk2, smartport, printer, options.e);
initUI(apple2, disk2, smartport, printer, options.e);
}).catch(console.error);

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const apple2_charset = new Uint8Array([
0x00,0x1c,0x22,0x2a,0x2e,0x2c,0x20,0x1e,

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
/* exported apple2e_charset */

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const apple2enh_charset = new Uint8Array([
0x1c,0x22,0x2a,0x3a,0x1a,0x02,0x3c,0x00,

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const apple2j_charset = new Uint8Array([
0xff,0xef,0xe1,0xed,0xd5,0xfb,0xf7,0xef,

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const apple2lc_charset = new Uint8Array([
0x1c,0x22,0x2a,0x2a,0x2c,0x20,0x1e,0x00,

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const pigfont_charset = new Uint8Array([
0x00,0x1c,0x22,0x2a,0x2e,0x20,0x1e,0x00,

View File

@ -1,4 +1,4 @@
import { ReadonlyUint8Array } from '../types';
import { ReadonlyUint8Array } from '../../types';
const rmfont_charset = new Uint8Array([
0x3c,0x42,0x59,0x55,0x55,0x39,0x02,0x3c,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0x6f,0xd8,0x65,0xd7,0xf8,0xdc,0x94,0xd9,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0x6f, 0xd8, 0x65, 0xd7, 0xf8, 0xdc, 0x94, 0xd9,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0xa9, 0x20, 0x8d, 0x26, 0x03, 0xad, 0x57, 0xc0,

View File

@ -1,5 +1,5 @@
import { ReadonlyUint8Array } from '../types';
import ROM from './rom';
import { ReadonlyUint8Array } from '../../types';
import ROM from '../rom';
const rom = new Uint8Array([
0xa9, 0x20, 0x8d, 0x26, 0x03, 0xad, 0x57, 0xc0,

View File

@ -49,7 +49,14 @@ type DiskCollection = {
[name: string]: DiskDescriptor[]
};
const KNOWN_FILE_TYPES = [...DISK_FORMATS, ...TAPE_TYPES] as readonly string[];
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
const BIN_TYPES = ['bin'];
const KNOWN_FILE_TYPES = [
...DISK_FORMATS,
...TAPE_TYPES,
...BIN_TYPES,
] as readonly string[];
const disk_categories: DiskCollection = { 'Local Saves': [] };
const disk_sets: DiskCollection = {};
@ -73,6 +80,7 @@ let system: System;
let keyboard: KeyBoard;
let io: Apple2IO;
let _currentDrive: DriveNumber = 1;
let _e: boolean;
export const driveLights = new DriveLights();
@ -84,6 +92,7 @@ export function dumpAppleSoftProgram() {
export function compileAppleSoftProgram(program: string) {
const compiler = new ApplesoftCompiler(cpu);
compiler.compile(program);
dumpAppleSoftProgram();
}
export function openLoad(driveString: string, event: MouseEvent) {
@ -204,9 +213,9 @@ function loadingStop() {
MicroModal.close('loading-modal');
if (!paused) {
vm.ready.then(() => {
_apple2.ready.then(() => {
_apple2.run();
});
}).catch(console.error);
}
}
@ -297,11 +306,8 @@ export function doDelete(name: string) {
}
}
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
const BIN_TYPES = ['bin'];
interface LoadOptions {
address: word,
address?: word,
runOnLoad?: boolean,
}
@ -318,7 +324,8 @@ function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOption
} else if (includes(TAPE_TYPES, ext)) {
tape.doLoadLocalTape(file);
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
doLoadBinary(file, { address: parseInt(aux || '2000', 16), ...options });
const address = aux !== undefined ? parseInt(aux, 16) : undefined;
doLoadBinary(file, { address, ...options });
} else {
const addressInput = document.querySelector<HTMLInputElement>('#local_file_address');
const addressStr = addressInput?.value;
@ -342,6 +349,7 @@ function doLoadBinary(file: File, options: LoadOptions) {
fileReader.onload = function () {
const result = this.result as ArrayBuffer;
let { address } = options;
address = address ?? 0x2000;
const bytes = new Uint8Array(result);
for (let idx = 0; idx < result.byteLength; idx++) {
cpu.write(address >> 8, address & 0xff, bytes[idx]);
@ -349,7 +357,7 @@ function doLoadBinary(file: File, options: LoadOptions) {
}
if (options.runOnLoad) {
cpu.reset();
cpu.setPC(options.address);
cpu.setPC(address);
}
loadingStop();
};
@ -437,9 +445,10 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
initGamepad();
}
} else {
if (includes(DISK_FORMATS, ext)
&& _disk2.setBinary(drive, name, ext, data)) {
initGamepad();
if (includes(DISK_FORMATS, ext)) {
if (_disk2.setBinary(drive, name, ext, data)) {
initGamepad();
}
} else {
throw new Error(`Extension ${ext} not recognized.`);
}
@ -678,38 +687,40 @@ declare global {
}
}
let oldCat = '';
let option;
for (let idx = 0; idx < window.disk_index.length; idx++) {
const file = window.disk_index[idx];
const cat = file.category;
const name = file.name;
const disk = file.disk;
if (file.e && !window.e) {
continue;
}
if (cat != oldCat) {
option = document.createElement('option');
option.value = cat;
option.innerText = cat;
categorySelect.append(option);
disk_categories[cat] = [];
oldCat = cat;
}
disk_categories[cat].push(file);
if (disk) {
if (!disk_sets[name]) {
disk_sets[name] = [];
function buildDiskIndex() {
let oldCat = '';
let option;
for (let idx = 0; idx < window.disk_index.length; idx++) {
const file = window.disk_index[idx];
const cat = file.category;
const name = file.name;
const disk = file.disk;
if (file.e && !_e) {
continue;
}
disk_sets[name].push(file);
}
}
option = document.createElement('option');
option.innerText = 'Local Saves';
categorySelect.append(option);
if (cat != oldCat) {
option = document.createElement('option');
option.value = cat;
option.innerText = cat;
categorySelect.append(option);
updateLocalStorage();
disk_categories[cat] = [];
oldCat = cat;
}
disk_categories[cat].push(file);
if (disk) {
if (!disk_sets[name]) {
disk_sets[name] = [];
}
disk_sets[name].push(file);
}
}
option = document.createElement('option');
option.innerText = 'Local Saves';
categorySelect.append(option);
updateLocalStorage();
}
/**
* Processes the URL fragment. It is expected to be of the form:
@ -751,9 +762,9 @@ export function updateUI() {
export function pauseRun() {
const label = document.querySelector<HTMLElement>('#pause-run i')!;
if (paused) {
vm.ready.then(() => {
_apple2.ready.then(() => {
_apple2.run();
});
}).catch(console.error);
label.classList.remove('fa-play');
label.classList.add('fa-pause');
} else {
@ -805,6 +816,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
_disk2 = disk2;
_smartPort = smartPort;
_printer = printer;
_e = e;
system = new System(io, e);
optionsModal.addOptions(system);
@ -835,6 +847,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
}
});
buildDiskIndex();
/*
* Input Handling
*/
@ -865,9 +879,9 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
_apple2.stop();
processHash(hash);
} else {
vm.ready.then(() => {
_apple2.ready.then(() => {
_apple2.run();
});
}).catch(console.error);
}
document.querySelector<HTMLInputElement>('#local_file')?.addEventListener(

View File

@ -89,15 +89,20 @@ export class Audio implements OptionHandler {
autoStart = () => {
if (this.audioContext && !this.started) {
this.samples = [];
this.audioContext.resume();
this.started = true;
this.audioContext.resume().then(() => {
this.started = true;
}).catch((error) => {
console.warn('audio not started', error);
});
}
}
start = () => {
if (this.audioContext) {
this.samples = [];
this.audioContext.resume();
this.audioContext.resume().catch((error) => {
console.warn('audio not resumed', error);
});
}
}

View File

@ -36,7 +36,7 @@ export default class Tape {
fileReader.onload = (ev: ProgressEvent) => {
const target: FileReader = ev.target as FileReader;
const result: ArrayBuffer = target.result as ArrayBuffer;
context.decodeAudioData(result, (buffer) => {
context.decodeAudioData(result).then((buffer) => {
const buf: TapeData = [];
const data = buffer.getChannelData(0);
let datum = data[0];

View File

@ -2,7 +2,7 @@
import { VideoPage } from 'js/videomodes';
import { LoresPage2D, HiresPage2D, VideoModes2D } from 'js/canvas';
import apple2enh_char from 'js/roms/apple2enh_char';
import apple2enh_char from 'js/roms/character/apple2enh_char';
import { createImageFromImageData } from 'test/util/image';
function checkImageData(page: VideoPage) {

View File

@ -2,7 +2,7 @@
import { VideoPage } from 'js/videomodes';
import { LoresPageGL, HiresPageGL, VideoModesGL } from 'js/gl';
import apple2enh_char from 'js/roms/apple2enh_char';
import apple2enh_char from 'js/roms/character/apple2enh_char';
import { createImageFromImageData } from 'test/util/image';
function checkImageData(page: VideoPage) {

View File

@ -1,10 +1,10 @@
import type ROM from '../../js/roms/rom';
import OriginalROM from '../../js/roms/original';
import IntegerROM from '../../js/roms/intbasic';
import FPBasicROM from '../../js/roms/fpbasic';
import Apple2eROM from '../../js/roms/apple2e';
import Apple2enhROM from '../../js/roms/apple2enh';
import Apple2jROM from '../../js/roms/apple2j';
import OriginalROM from '../../js/roms/system/original';
import IntegerROM from '../../js/roms/system/intbasic';
import FPBasicROM from '../../js/roms/system/fpbasic';
import Apple2eROM from '../../js/roms/system/apple2e';
import Apple2enhROM from '../../js/roms/system/apple2enh';
import Apple2jROM from '../../js/roms/system/apple2j';
const roms: { [name: string]: { new(): ROM } } = {
'original': OriginalROM,

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "esnext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
@ -30,5 +30,6 @@
"include": [
"js/**/*",
"test/**/*",
"*.config.js"
]
}

View File

@ -10,6 +10,8 @@ module.exports =
},
output: {
path: path.resolve('dist/'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
library: {
name: 'Apple2',
type: 'umd',
@ -54,7 +56,7 @@ module.exports =
},
],
exclude: /node_modules/,
}
},
],
},
resolve: {