handle failed BIOS loads more gracefully

This commit is contained in:
Steven Hugg committed 2026-10-23 18:46:16 +02:00
1 parent 0988fa9136
commit e109fd4f4f
4 files changed
+143 -4

No files matched your search

+5
View File
@@ -125,6 +125,11 @@ export abstract class BaseWASMMachine {
this.reset(); // TODO?
}
loadBIOS(srcArray: Uint8Array) {
// the machine's ROM layout is fixed, so a bigger image can't work;
// TypedArray.set() would only throw a cryptic "source array is too long"
if (srcArray.length > this.biosarr.length) {
throw new Error(`BIOS too large: ${srcArray.length} bytes, but this machine's BIOS is only ${this.biosarr.length} bytes`);
}
this.biosarr.set(srcArray);
// machine_init copied the ROMs out of biosarr, so a BIOS loaded after
// that has to build a new machine (the old one is leaked)
+9 -1
View File
@@ -1086,7 +1086,15 @@ async function loadBIOSFromProject() {
var biosdata = await store.getItem(biospath);
if (biosdata instanceof Uint8Array) {
console.log('loading BIOS', biospath, biosdata.length + " bytes")
platform.loadBIOS(biospath, biosdata);
try {
platform.loadBIOS(biospath, biosdata);
} catch (e) {
// A bad BIOS file must not lock us out of the IDE (and its file manager):
// the platform keeps running with its built-in BIOS, so we warn and go on.
console.log(e);
alertError(`Could not load BIOS file "${biospath}": ${e.message || e + ''}<br><br>` +
'The emulator will use its built-in BIOS. Delete or rename the file to fix this.');
}
} else {
console.log('BIOS file must be binary')
}
+80 -3
View File
@@ -7,10 +7,26 @@
import { Machine } from "../common/baseplatform";
import { Probeable, TrapCondition } from "../common/devices";
import { KeyFlags } from "../common/emu";
import { hex } from "../common/util";
import { KeyFlags, dumpRAM } from "../common/emu";
import { hex, lpad } from "../common/util";
import { BaseWASMMachine } from "../common/wasmplatform";
// Offsets of the registers in the C structs that machine_get_debug_pointer()
// returns, for the debug panes. The chips structs only hold integers, so the
// offsets are the same in every WASM build of the machine.
const VIA_PA = 0x00; // m6522_port_t { inpr, outr, ddr, pins }
const VIA_PB = 0x0a;
const VIA_T1 = 0x14; // m6522_timer_t { latch, counter, ... }
const VIA_T2 = 0x1c;
const VIA_IER = 0x24; // m6522_int_t { ier, ifr, pip }
const VIA_IFR = 0x25;
const VIA_ACR = 0x28;
const VIA_PCR = 0x29;
const VIA_SIZE = 56;
// m6561_t starts with { uint64 pins, fetch_cb, user_data, bool debug_vis },
// and pointers are 32 bits in WASM32, so regs[16] starts at byte 17.
const VIC_REGS = 17;
export class VIC20_WASMMachine extends BaseWASMMachine implements Machine, Probeable {
numTotalScanlines = 312;
@@ -139,12 +155,25 @@ export class VIC20_WASMMachine extends BaseWASMMachine implements Machine, Probe
o:this.readConst(pc),
}
}
getDebugStateOffset(index: number) {
var p = this.exports.machine_get_debug_pointer(this.sys, index);
return p - this.sys;
}
saveState() {
this.exports.machine_save_state(this.sys, this.stateptr);
// the debug panes want the chip registers at the moment of the save, so
// they travel in the state instead of being read from the live machine
let ram0 = this.getDebugStateOffset(5);
let via1 = this.getDebugStateOffset(1);
let via2 = this.getDebugStateOffset(2);
let vic = this.getDebugStateOffset(3) + VIC_REGS;
return {
c:this.getCPUState(),
state:this.statearr.slice(0),
ram:this.statearr.slice(18640, 18640+0x200), // ZP and stack (TODO)
ram:this.statearr.slice(ram0, ram0+0x200), // ZP and stack
via1:this.statearr.slice(via1, via1+VIA_SIZE),
via2:this.statearr.slice(via2, via2+VIA_SIZE),
vic:this.statearr.slice(vic, vic+16),
// the joystick mask lives on this object, not in the WASM state blob,
// so it has to be saved explicitly or it leaks across a rewind
joymask0: this.joymask0,
@@ -161,6 +190,54 @@ export class VIC20_WASMMachine extends BaseWASMMachine implements Machine, Probe
getVideoParams() {
return {width:232, height:272, overscan:true, videoFrequency:50, aspect:1.5};
}
getDebugCategories() {
return ['CPU','ZPRAM','Stack','VIA','VIC'];
}
// translate an address as the VIC-I sees it (14 bits, its own memory map)
// into the CPU address of the same memory, or "ROM" for the character ROM
vicAddressToCPU(addr: number) {
if (addr < 0x2000) return 'ROM $' + hex(0x8000 + (addr & 0xfff), 4);
return '$' + hex((addr - 0x2000) & 0xffff, 4);
}
// format one VIA, in the style of the C64's CIA pane
viaToString(name: string, m: Uint8Array) {
let t1 = m[VIA_T1+2] + m[VIA_T1+3]*256;
let t2 = m[VIA_T2+2] + m[VIA_T2+3]*256;
let s = `${name}\n`;
s += ` A: Out ${hex(m[VIA_PA+1])} In ${hex(m[VIA_PA+0])} DDR ${hex(m[VIA_PA+2])}`;
s += ` T1 ${hex(t1, 4)}\n`;
s += ` B: Out ${hex(m[VIA_PB+1])} In ${hex(m[VIA_PB+0])} DDR ${hex(m[VIA_PB+2])}`;
s += ` T2 ${hex(t2, 4)}\n`;
s += ` ACR ${hex(m[VIA_ACR])} PCR ${hex(m[VIA_PCR])}`;
s += ` IER ${hex(m[VIA_IER])} IFR ${hex(m[VIA_IFR])}\n`;
return s;
}
getDebugInfo(category: string, state: any) {
switch (category) {
case 'VIA': {
let s = '';
s += this.viaToString('VIA 1 ($9110) joystick, cassette, NMI', state.via1);
s += this.viaToString('VIA 2 ($9120) keyboard, cassette, IRQ', state.via2);
return s;
}
case 'VIC': {
let m = state.vic;
// screen memory is at A13..A9 from $9005 bits 4-7 and $9002 bit 7
// (A8 must be 0, the screen needs a 512 byte page)
let screen = ((m[5] & 0xf0) << 6) | ((m[2] & 0x80) << 2);
// character memory is the $9005 low nibble, in 1K steps
let charmem = (m[5] & 0x0f) << 10;
let s = 'Mode: TEXT\n';
s += `Raster: ${lpad(this.getRasterY(), 3)}`;
s += ` Border ${m[15] & 7} Background ${(m[15]>>4) & 0xf} Aux ${(m[14]>>4) & 0xf}`;
s += ` Volume ${m[14] & 0xf}\n`;
s += `Screen: ${this.vicAddressToCPU(screen)} `;
s += `Char: ${this.vicAddressToCPU(charmem)}\n`;
s += dumpRAM(m, 0x9000, 16);
return s;
}
}
}
setKeyInput(key: number, code: number, flags: number): void {
// TODO: handle shifted keys
if (key == 16 || key == 17 || key == 18 || key == 224) return; // meta keys
+49
View File
@@ -0,0 +1,49 @@
import assert from "assert";
import { describe, it } from "mocha";
import * as fs from "fs";
// .gitmodules lives at the repo root, which is the cwd for `npm test`.
const GITMODULES = '.gitmodules';
interface Submodule { name: string, path: string, url: string }
function parseGitmodules(text: string): Submodule[] {
var out: Submodule[] = [];
var cur: any = null;
for (var line of text.split('\n')) {
var m;
if ((m = line.match(/^\s*\[\s*submodule\s+"([^"]+)"\s*\]/))) {
cur = { name: m[1] };
out.push(cur);
} else if (cur && (m = line.match(/^\s*(\w+)\s*=\s*(.*?)\s*$/))) {
cur[m[1]] = m[2];
}
}
return out;
}
// git resolves a relative URL ("./chips") against the superproject's remote,
// so "./chips" in a repo hosted at github.com/org/repo becomes
// github.com/org/repo/chips -- a 404 that breaks every CI clone.
function isRelativeURL(url: string) {
return url.startsWith('./') || url.startsWith('../') || !/^[a-z][a-z0-9+.-]*:\/\/|^[^@\/]+@[^:\/]+:./i.test(url);
}
describe('submodules', () => {
it('.gitmodules exists and lists submodules', function () {
if (!fs.existsSync(GITMODULES)) this.skip();
var subs = parseGitmodules(fs.readFileSync(GITMODULES, 'utf8'));
assert.ok(subs.length > 0, 'no submodules parsed from .gitmodules');
});
it('uses absolute URLs that do not depend on the superproject remote', function () {
if (!fs.existsSync(GITMODULES)) this.skip();
var subs = parseGitmodules(fs.readFileSync(GITMODULES, 'utf8'));
for (var sub of subs) {
assert.ok(sub.path, `${sub.name}: missing path`);
assert.ok(sub.url, `${sub.name}: missing url`);
assert.ok(!isRelativeURL(sub.url),
`${sub.name}: relative url "${sub.url}" resolves against the superproject remote and breaks CI; use e.g. https://github.com/8bitworkshop/${sub.name}`);
}
});
});