diff --git a/src/common/emucore.ts b/src/common/emucore.ts index f9510da9..9b3411ec 100644 --- a/src/common/emucore.ts +++ b/src/common/emucore.ts @@ -401,6 +401,9 @@ export class EmuCore { } getVideo(): VideoOutput | null { + // verilog sets hasvideo on ROM load; a design with no vsync/hsync/rgb + // leaves the headless raster blank, which isn't a failure to draw + if ((this.platform as any).hasvideo === false) return null; return this.captured?.() ?? this.video?.get() ?? null; } diff --git a/src/platform/williams.ts b/src/platform/williams.ts index 2ab27511..67bc8796 100644 --- a/src/platform/williams.ts +++ b/src/platform/williams.ts @@ -303,14 +303,17 @@ var WilliamsPlatform = function(mainElement, proto, options) { cpu = self.newCPU(membus, iobus); audio = new MasterAudio(); - worker = new Worker("./src/common/audio/z80worker.js"); - workerchannel = new WorkerSoundChannel(worker); - audio.master.addChannel(workerchannel); + // the sound board runs in a web worker; headless (node) has no sound + if (typeof Worker !== 'undefined') { + worker = new Worker("./src/common/audio/z80worker.js"); + workerchannel = new WorkerSoundChannel(worker); + audio.master.addChannel(workerchannel); + } let rotate = options?.rotate == null ? -90 : parseFloat(options.rotate); video = new RasterVideo(mainElement, SCREEN_WIDTH, SCREEN_HEIGHT, { rotate }); video.create(); - $(video.canvas).click(function(e) { + if (typeof $ !== 'undefined') $(video.canvas).click(function(e) { var x = Math.floor(e.offsetX * video.canvas.width / $(video.canvas).width()); var y = Math.floor(e.offsetY * video.canvas.height / $(video.canvas).height()); var addr = (x >> 3) + (y * 32) + 0x400; @@ -359,7 +362,7 @@ var WilliamsPlatform = function(mainElement, proto, options) { this.loadSoundROM = function(data) { console.log("loading sound ROM " + data.length + " bytes"); var soundrom = padBytes(data, 0x4000); - worker.postMessage({ rom: soundrom }); + if (worker) worker.postMessage({ rom: soundrom }); } this.loadROM = function(title, data) { diff --git a/src/tools/buildpresets.ts b/src/tools/buildpresets.ts index b81ee35a..c2fb9953 100644 --- a/src/tools/buildpresets.ts +++ b/src/tools/buildpresets.ts @@ -49,7 +49,7 @@ const PLATFORM_SRC_DIR = 'src/platform'; export interface RunResult { ok: boolean; // platform started and ran without throwing frames: number; // frames actually advanced - verdict: 'ok' | 'solid' | 'blank' | 'novideo' | 'halted' | 'error'; + verdict: 'ok' | 'solid' | 'blank' | 'novideo' | 'skipped' | 'halted' | 'error'; width?: number; height?: number; colors?: number; // distinct pixel colors on the last frame @@ -93,8 +93,8 @@ const DEFAULT_TIMEOUT = 120000; const DEFAULT_RUN_FRAMES = 300; // A frame this uniform after a few seconds almost always means nothing drew; -// a real screen has a background plus text or sprites. -const SOLID_FRACTION = 0.999; +// a real screen has a background plus text or sprites (or stars). +const SOLID_FRACTION = 0.9995; // skip these platforms, they aren't ready yet or otherwise broken const SKIP_PLATFORMS = [ @@ -105,6 +105,10 @@ const SKIP_PLATFORMS = [ 'vectrex', ]; +// build these, but don't run them: the emulator needs a real browser +// (x86 is the v86 PC emulator, loaded as a page script) +const NO_RUN_PLATFORMS = ['x86']; + // Load every platform module so it registers itself in PLATFORMS. A couple of // them touch the DOM at import time and can't run here; they're reported // rather than hidden. @@ -482,6 +486,9 @@ export async function runPreset( const started = Date.now(); const frames = opts.frames || DEFAULT_RUN_FRAMES; const run: RunResult = { ok: false, frames, verdict: 'error', ms: 0 }; + if (NO_RUN_PLATFORMS.includes(platform)) { + return { ...run, ok: true, frames: 0, verdict: 'skipped' }; + } const capture = opts.verbose ? null : captureOutput(); let target: EmuTarget | null = null; try { @@ -630,6 +637,7 @@ function runStatus(run: RunResult): string { case 'ok': return green('run ok') + ' ' + dim(info); case 'halted': return dim('run halted') + ' ' + dim(info); case 'novideo': return dim('run: no video'); + case 'skipped': return dim('run: skipped (needs browser)'); case 'blank': return yellow(bold('run BLANK')) + ' ' + dim(info); case 'solid': return yellow(bold('run SOLID')) + ' ' + dim(info); default: return red(bold('run FAIL')) + ' ' + dim(info); @@ -690,7 +698,7 @@ async function main() { console.log(bold(`${run ? 'building and running' : 'building'} ${presets.length} presets...`)); const results = await buildAllPresets({ presets, timeout, verbose, run, frames, pngDir, onResult: (r) => { - const clean = !r.run || r.run.verdict === 'ok' || r.run.verdict === 'novideo' || r.run.verdict === 'halted'; + const clean = !r.run || r.run.verdict === 'ok' || r.run.verdict === 'novideo' || r.run.verdict === 'skipped' || r.run.verdict === 'halted'; if (quiet && r.ok && clean) return; const status = r.ok ? green('ok ') : red(bold('FAIL')); const size = r.size != null ? `${r.size} bytes` : ''; @@ -729,7 +737,7 @@ async function main() { // what the builds did when loaded, separately from whether they built const ran = results.filter((r) => r.run); if (ran.length) { - const problems = ran.filter((r) => r.run.verdict !== 'ok' && r.run.verdict !== 'novideo' && r.run.verdict !== 'halted'); + const problems = ran.filter((r) => r.run.verdict !== 'ok' && r.run.verdict !== 'novideo' && r.run.verdict !== 'skipped' && r.run.verdict !== 'halted'); const tally2 = `${ran.length - problems.length}/${ran.length} presets run clean`; console.log('\n' + bold(problems.length ? yellow(tally2) : green(tally2))); if (problems.length) {