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:
@@ -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"})
|
||||
|
||||
@@ -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
58
src/tools/nodemock.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
BIN
test/roms/gb/cpu_instrs.gb
Normal file
Binary file not shown.
Reference in New Issue
Block a user