1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2026-03-10 21:25:31 +00:00

nes: removed JQuery

cli: using nodemock.ts utilities, added --info and --memdump
This commit is contained in:
Steven Hugg
2026-03-05 12:01:53 +01:00
parent 231f9ac9dd
commit b85b2ffbb7
6 changed files with 191 additions and 58 deletions

View File

@@ -1,7 +1,7 @@
import { Platform, Base6502Platform, getOpcodeMetadata_6502, getToolForFilename_6502, Preset } from "../common/baseplatform";
import { PLATFORMS, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, KeyFlags, EmuHalt, ControllerPoller } from "../common/emu";
import { hex, byteArrayToString } from "../common/util";
import { hex, byteArrayToString, replaceAll } from "../common/util";
import { CodeAnalyzer_nes } from "../common/analysis";
import { SampleAudio } from "../common/audio";
import { ProbeRecorder } from "../common/probe";
@@ -71,16 +71,17 @@ const JSNES_KEYCODE_MAP = makeKeycodeMap([
class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
mainElement;
mainElement : HTMLElement;
nes;
video;
video: RasterVideo;
audio;
timer;
timer: AnimationTimer;
poller : ControllerPoller;
audioFrequency = 44030; //44100
frameindex = 0;
ntvideo;
ntlastbuf;
ntvideo: RasterVideo;
ntlastbuf: Uint32Array;
showDebugView = false;
machine = { cpuCyclesPerLine: 114 }; // TODO: hack for width of probe scope
@@ -93,21 +94,22 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
start() {
this.debugPCDelta = 1;
var debugbar = $("<div>").appendTo(this.mainElement);
this.audio = new SampleAudio(this.audioFrequency);
this.video = new RasterVideo(this.mainElement,256,224,{overscan:true});
this.video.create();
// debugging view
this.ntvideo = new RasterVideo(this.mainElement,512,480,{overscan:false});
this.ntvideo.create();
$(this.ntvideo.canvas).hide();
this.ntvideo.canvas.style.display = 'none';
this.ntlastbuf = new Uint32Array(0x1000);
if (Mousetrap.bind) Mousetrap.bind('ctrl+shift+alt+n', () => {
$(this.video.canvas).toggle()
$(this.ntvideo.canvas).toggle()
this.showDebugView = !this.showDebugView;
this.video.canvas.style.display = !this.showDebugView ? '' : 'none';
this.ntvideo.canvas.style.display = this.showDebugView ? '' : 'none';
});
// toggle buttons (TODO)
/*
var debugbar = $("<div>").appendTo(this.mainElement);
$('<button>').text("Video").appendTo(debugbar).click(() => { $(this.video.canvas).toggle() });
$('<button>').text("Nametable").appendTo(debugbar).click(() => { $(this.ntvideo.canvas).toggle() });
*/
@@ -162,12 +164,12 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
advance(novideo : boolean) : number {
this.nes.frame();
return 29780; //TODO
return 29780; //TODO: PAL or NTSC?
}
updateDebugViews() {
// don't update if view is hidden
if (! $(this.ntvideo.canvas).is(":visible"))
if (!this.showDebugView)
return;
var a = 0;
var attraddr = 0;
@@ -496,28 +498,29 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
getDebugSymbolFile() {
var sym = this.debugSymbols.addr2symbol;
var text = "";
$.each(sym, function(k, v) {
for (let [k, v] of Object.entries(sym)) {
let numK = Number(k);
let symType;
if (k < 0x2000) {
k = k % 0x800;
if (numK < 0x2000) {
numK = numK % 0x800;
symType = "R";
} else if (k < 0x6000) symType = "G";
else if (k < 0x8000) {
k = k - 0x6000;
} else if (numK < 0x6000) symType = "G";
else if (numK < 0x8000) {
numK = numK - 0x6000;
symType = "S";
} else {
k = k - 0x8000;
} else {
numK = numK - 0x8000;
symType = "P";
}
let addr = Number(k).toString(16).padStart(4, '0').toUpperCase();
let addr = numK.toString(16).padStart(4, '0').toUpperCase();
// Mesen doesn't allow lables to start with digits
if (v[0] >= '0' && v[0] <= '9') {
v = "L" + v;
if ((v as string)[0] >= '0' && (v as string)[0] <= '9') {
v = "L" + v;
}
// nor does it allow dots
v = (v as any).replaceAll('.', '_');
v = replaceAll(v, '.', '_');
text += `${symType}:${addr}:${v}\n`;
});
}
return {
extension:".mlb",
blob: new Blob([text], {type:"text/plain"})

View File

@@ -4,6 +4,8 @@
import * as fs from 'fs';
import { initialize, compile, compileSourceFile, preload, listTools, listPlatforms, getToolForFilename, PLATFORM_PARAMS, TOOLS, TOOL_PRELOADFS } from './testlib';
import { isDebuggable } from '../common/baseplatform';
import { hex } from '../common/util';
interface CLIResult {
success: boolean;
@@ -150,6 +152,8 @@ function formatGeneric(data: any): void {
}
}
var BOOLEAN_FLAGS = new Set(['json', 'info']);
function parseArgs(argv: string[]): { command: string; args: { [key: string]: string }; positional: string[] } {
var command = argv[2];
var args: { [key: string]: string } = {};
@@ -158,7 +162,9 @@ function parseArgs(argv: string[]): { command: string; args: { [key: string]: st
for (var i = 3; i < argv.length; i++) {
if (argv[i].startsWith('--')) {
var key = argv[i].substring(2);
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
if (BOOLEAN_FLAGS.has(key)) {
args[key] = 'true';
} else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
args[key] = argv[++i];
} else {
args[key] = 'true';
@@ -179,7 +185,7 @@ function usage(): void {
commands: {
'compile': 'compile --platform <platform> [--tool <tool>] [--output <file>] <source>',
'check': 'check --platform <platform> [--tool <tool>] <source>',
'run': 'run (--platform <id> | --machine <module:ClassName>) [--frames N] [--output <file.png>] <rom>',
'run': 'run (--platform <id> | --machine <module:ClassName>) [--frames N] [--output <file.png>] [--memdump start,end] [--info] <rom>',
'list-tools': 'list-tools',
'list-platforms': 'list-platforms',
}
@@ -301,11 +307,13 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
var romData = new Uint8Array(fs.readFileSync(romFile));
var pixels: Uint32Array | null = null;
var vid: { width: number; height: number } | null = null;
var platformRunner: any = null;
var machineInstance: any = null;
if (platformId) {
// Platform mode: load platform module, mock video, run via Platform API
var { PlatformRunner, loadPlatform } = await import('./runmachine');
var platformRunner = new PlatformRunner(await loadPlatform(platformId));
platformRunner = new PlatformRunner(await loadPlatform(platformId));
await platformRunner.start();
platformRunner.loadROM("ROM", romData);
for (var i = 0; i < frames; i++) {
@@ -326,7 +334,7 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
}
var [modname, clsname] = parts;
var { MachineRunner, loadMachine } = await import('./runmachine');
var machineInstance = await loadMachine(modname, clsname);
machineInstance = await loadMachine(modname, clsname);
var runner = new MachineRunner(machineInstance);
runner.setup();
machineInstance.loadROM(romData);
@@ -337,19 +345,6 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
vid = pixels ? (machineInstance as any).getVideoParams() : null;
}
// Encode framebuffer as PNG if video is available
var pngData: Uint8Array | null = null;
if (pixels && vid) {
var { encode } = await import('fast-png');
var rgba = new Uint8Array(pixels.buffer);
pngData = encode({ width: vid.width, height: vid.height, data: rgba, channels: 4 });
}
// Write PNG to file if requested
if (outputFile && pngData) {
fs.writeFileSync(outputFile, pngData);
}
output({
success: true,
command: 'run',
@@ -364,6 +359,90 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
}
});
// --info: print debug info for all categories + disassembly at PC
if (args['info'] === 'true') {
var plat = platformId ? platformRunner.platform : null;
var mach = machine ? machineInstance : null;
var debugTarget: any = plat || mach;
if (debugTarget && isDebuggable(debugTarget)) {
var state = plat?.saveState?.() ?? mach?.saveState?.();
if (state) {
var categories = debugTarget.getDebugCategories();
for (var cat of categories) {
var info = debugTarget.getDebugInfo(cat, state);
if (info) {
process.stderr.write(`${c.bold}${c.magenta}[${cat}]${c.reset}\n`);
process.stderr.write(info);
if (!info.endsWith('\n')) process.stderr.write('\n');
}
}
}
}
// Disassembly around current PC
if (debugTarget?.getPC && debugTarget?.disassemble && debugTarget?.readAddress) {
var pc = debugTarget.getPC();
var readFn = (addr: number) => debugTarget.readAddress(addr);
process.stderr.write(`${c.bold}${c.magenta}[Disassembly]${c.reset}\n`);
var addr = pc;
for (var i = 0; i < 16; i++) {
var disasm = debugTarget.disassemble(addr, readFn);
var prefix = (addr === pc) ? `${c.green}>${c.reset}` : ' ';
// show hex bytes
var bytesStr = '';
for (var b = 0; b < disasm.nbytes; b++) {
bytesStr += hex(readFn(addr + b)) + ' ';
}
process.stderr.write(`${prefix}${c.cyan}$${hex(addr, 4)}${c.reset} ${c.dim}${bytesStr.padEnd(12)}${c.reset} ${disasm.line}\n`);
addr += disasm.nbytes;
}
}
}
// --memdump start,end: hexdump memory range
if (args['memdump']) {
var mdparts = args['memdump'].split(',');
var start = parseInt(mdparts[0], 16);
var end = parseInt(mdparts[1], 16);
if (isNaN(start) || isNaN(end) || end < start) {
output({ success: false, command: 'run', error: `Invalid --memdump range: ${args['memdump']} (use hex addresses like 0000,00ff)` });
process.exit(1);
}
var plat2 = platformId ? platformRunner.platform : null;
var mach2 = machine ? machineInstance : null;
var readFn2: ((addr: number) => number) | null = null;
if (plat2?.readAddress) readFn2 = (addr) => plat2.readAddress(addr);
else if (mach2 && typeof (mach2 as any).read === 'function') readFn2 = (addr) => (mach2 as any).read(addr);
if (!readFn2) {
output({ success: false, command: 'run', error: 'Platform/machine does not support readAddress' });
process.exit(1);
}
var len = end - start + 1;
for (var ofs = 0; ofs < len; ofs += 16) {
var line = `${c.cyan}$${hex(start + ofs, 4)}${c.reset}:`;
var ascii = '';
for (var i = 0; i < 16 && ofs + i < len; i++) {
if (i === 8) line += ' ';
var byte = readFn2(start + ofs + i);
line += ` ${hex(byte)}`;
ascii += (byte >= 0x20 && byte < 0x7f) ? String.fromCharCode(byte) : '.';
}
process.stderr.write(`${line} ${c.dim}${ascii}${c.reset}\n`);
}
}
// Encode framebuffer as PNG if video is available
var pngData: Uint8Array | null = null;
if (pixels && vid) {
var { encode } = await import('fast-png');
var rgba = new Uint8Array(pixels.buffer);
pngData = encode({ width: vid.width, height: vid.height, data: rgba, channels: 4 });
}
// Write PNG to file if requested
if (outputFile && pngData) {
fs.writeFileSync(outputFile, pngData);
}
// Display image in terminal if connected to a TTY
if (pngData && process.stdout.isTTY) {
var { displayImageInTerminal } = await import('./termimage');
@@ -443,6 +522,7 @@ async function main() {
process.exit(1);
}
} catch (e) {
console.log(e);
output({
success: false,
command: command,

58
src/tools/nodemock.ts Normal file
View File

@@ -0,0 +1,58 @@
import { SampledAudioSink } from "../common/devices";
import fs from 'fs';
export function mockGlobals() {
global.atob = require('atob');
global.btoa = require('btoa');
(global as any).window = global;
(global as any).window.addEventListener = (global as any).window.addEventListener || function () { };
(global as any).window.removeEventListener = (global as any).window.removeEventListener || function () { };
(global as any).document = (global as any).document || { addEventListener() { }, removeEventListener() { } };
try { (global as any).navigator = (global as any).navigator || {}; } catch (e) { }
}
export class NullAudio implements SampledAudioSink {
feedSample(value: number, count: number): void {
}
}
export function mockAudio() {
class NullPsgDeviceChannel {
setMode() { }
setDevice() { }
generate() { }
setBufferLength() { }
setSampleRate() { }
getBuffer() { return []; }
writeRegister() { }
writeRegisterSN() { }
writeRegisterAY() { }
}
class NullMasterChannel {
addChannel() { }
}
global.MasterChannel = NullMasterChannel;
global.PsgDeviceChannel = NullPsgDeviceChannel;
}
export function mockFetch() {
global.fetch = async (path, init) => {
let bin = fs.readFileSync(path);
let blob = new Blob([bin]);
return new Response(blob);
}
}
export function mockDOM() {
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><div id="emulator"><div id="javatari-screen"></div></div>`);
global.window = dom.window;
global.document = dom.window.document;
//global['$'] = require("jquery/jquery.min.js");
dom.window.Audio = null;
//global.Image = function () { };
return dom;
}

View File

@@ -1,24 +1,10 @@
import { hasAudio, hasSerialIO, hasVideo, Machine, Platform } from "../common/baseplatform";
import { SampledAudioSink, SerialIOInterface } from "../common/devices";
import { SerialIOInterface } from "../common/devices";
import { PLATFORMS } from "../common/emu";
import * as emu from "../common/emu";
import { getRootBasePlatform } from "../common/util";
global.atob = require('atob');
global.btoa = require('btoa');
if (typeof window === 'undefined') {
(global as any).window = global;
(global as any).window.addEventListener = (global as any).window.addEventListener || function () { };
(global as any).window.removeEventListener = (global as any).window.removeEventListener || function () { };
(global as any).document = (global as any).document || { addEventListener() { }, removeEventListener() { } };
}
try { (global as any).navigator = (global as any).navigator || {}; } catch (e) { }
class NullAudio implements SampledAudioSink {
feedSample(value: number, count: number): void {
}
}
import { mockAudio, mockDOM, mockFetch, mockGlobals, NullAudio } from "./nodemock";
// TODO: merge with platform
class SerialTestHarness implements SerialIOInterface {
@@ -91,6 +77,7 @@ function installHeadlessVideo() {
this.fillRect = function () { };
this.fillStyle = '';
this.putImageData = function () { };
this.style = {};
};
(emu as any).VectorVideo = function (_mainElement: any, _width: number, _height: number, _options?: any) {
this.create = function () { this.drawops = 0; };
@@ -115,6 +102,10 @@ export class PlatformRunner {
private headless: ReturnType<typeof installHeadlessVideo>;
constructor(platform: Platform) {
mockGlobals();
mockAudio();
mockFetch();
mockDOM();
this.platform = platform;
this.headless = installHeadlessVideo();
}

View File

@@ -83,6 +83,7 @@ emu.RasterVideo = function(mainElement, width, height, options) {
this.fillRect = function() { }
this.fillStyle = '';
this.putImageData = function() { }
this.style = {};
}
emu.VectorVideo = function(mainElement, width, height, options) {

BIN
test/roms/gb/cpu_instrs.gb Normal file

Binary file not shown.