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}`); + } + }); +});