diff --git a/src/common/wasmplatform.ts b/src/common/wasmplatform.ts
index 56d2dc03..47b3bd07 100644
--- a/src/common/wasmplatform.ts
+++ b/src/common/wasmplatform.ts
@@ -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)
diff --git a/src/ide/ui.ts b/src/ide/ui.ts
index 2fd36cb8..f7899e8d 100644
--- a/src/ide/ui.ts
+++ b/src/ide/ui.ts
@@ -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 + ''}
` +
+ 'The emulator will use its built-in BIOS. Delete or rename the file to fix this.');
+ }
} else {
console.log('BIOS file must be binary')
}
diff --git a/src/machine/vic20.ts b/src/machine/vic20.ts
index f11ed991..c8f2260c 100644
--- a/src/machine/vic20.ts
+++ b/src/machine/vic20.ts
@@ -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
diff --git a/test/unit/testsubmodules.ts b/test/unit/testsubmodules.ts
new file mode 100644
index 00000000..a844f1d3
--- /dev/null
+++ b/test/unit/testsubmodules.ts
@@ -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}`);
+ }
+ });
+});