diff --git a/src/common/emu.ts b/src/common/emu.ts index dc066ea2..216777ac 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -618,6 +618,7 @@ export class ControllerPoller { poll() { if (!this.active) return; var gamepads = navigator.getGamepads && navigator.getGamepads(); + if (!gamepads) return; for (var gpi = 0; gpi < gamepads.length; gpi++) { let state = this.state[gpi]; let lastState = this.lastState[gpi]; diff --git a/src/docs/memmap.md b/src/docs/memmap.md index 8f94bee7..7f0ecb78 100644 --- a/src/docs/memmap.md +++ b/src/docs/memmap.md @@ -9,13 +9,13 @@ which most linker-based toolchains provide. ## Columns -- **Memory** shows the platform's native memory map (RAM, ROM, I/O). +- **System** shows the platform's native memory map (RAM, ROM, I/O). Gaps between areas appear as dashed "unmapped" bars, so the column covers the whole address space. - **Segments** shows the segments the linker produced. Free space between segments appears as an empty bar. -- **Variables** shows large variables and arrays (16 bytes or more) in - RAM. Sizes come from the compiler when it reports them (oscar64); +- **Objects** shows large variables and arrays (16 bytes or more) in + RAM. Sizes come from the toolchain when it reports them (cc65, oscar64); otherwise they are estimated from the distance to the next symbol. A column is hidden when it has nothing to show. Overlapping areas (for diff --git a/src/ide/views/memmaplayout.ts b/src/ide/views/memmaplayout.ts index 8bb2ed45..7c004bb0 100644 --- a/src/ide/views/memmaplayout.ts +++ b/src/ide/views/memmaplayout.ts @@ -94,7 +94,7 @@ function isHiddenSymbol(name: string) { // since its extent is unknown. export function findLargeVariables(symbolmap: { [sym: string]: number }, segments: Segment[], opts?: MemMapOptions, symbolsizes?: { [sym: string]: number }): MemMapBlock[] { - const minSize = opts?.minVarSize ?? 16; + const minSize = opts?.minVarSize ?? 128; const maxVars = opts?.maxVars ?? 64; const ramsegs = segments.filter(s => s.type == 'ram' && s.size > 0); if (!symbolmap || !ramsegs.length) return []; @@ -150,7 +150,7 @@ export function computeMemoryMapLayout(segments: Segment[], symbolmap?: { [sym: } const vars = findLargeVariables(symbolmap, segments, opts, symbolsizes); if (vars.length) { - columns.push(makeColumn('vars', 'Variables', vars, maxLanes)); + columns.push(makeColumn('vars', 'Objects', vars, maxLanes)); } const bset = new Set(); for (const col of columns) diff --git a/src/worker/platforms.ts b/src/worker/platforms.ts index 355c08bf..042d29e9 100644 --- a/src/worker/platforms.ts +++ b/src/worker/platforms.ts @@ -207,6 +207,8 @@ export var PLATFORM_PARAMS = { extra_link_files: ['crt0.o', 'neslib2.lib', 'neslib2.cfg', 'nesbanked.cfg'], // source link symbol -> linker config (was hardcoded in fixParamsWithDefines) symbolConfigs: { NES_MAPPER: { '4': 'nesbanked.cfg' } }, + // iNES header and CHR data are not in CPU address space + ignore_segments: ['HEADER', 'CHARS'], wiz_rom_ext: '.nes', }, 'apple2': { @@ -231,6 +233,8 @@ export var PLATFORM_PARAMS = { cfgfile: 'atari.cfg', libargs: ['atari.lib'], fastbasic_cfgfile: 'fastbasic-cart.cfg', + // XEX file headers and trailer are not in CPU address space + ignore_segments: ['EXEHDR', 'SYSCHKHDR', 'SYSCHKTRL', 'MAINHDR', 'AUTOSTRT'], }, 'atari8-800xl': { arch: '6502', diff --git a/src/worker/tools/cc65.ts b/src/worker/tools/cc65.ts index 34c857ce..4d67a8ed 100644 --- a/src/worker/tools/cc65.ts +++ b/src/worker/tools/cc65.ts @@ -4,6 +4,7 @@ import { getRootBasePlatform } from "../../common/util"; import { CodeListingMap, WorkerError } from "../../common/workertypes"; import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, fixParamsWithDefines, applyAsmProjectParams, putWorkFile, populateExtraFiles, store, populateEntry, anyTargetChanged, processEmbedDirective } from "../builder"; import { re_crlf, makeErrorMatcher } from "../listingutils"; +import { parseCC65DbgSizes } from "./cc65dbg"; import { loadNative, moduleInstFn, print_fn, setupFS, execMain, execToFile, emglobal, EmscriptenModule } from "../wasmutils"; @@ -190,7 +191,7 @@ export function linkLD65(step: BuildStep): BuildStepResult { '--lib-path', '/share/lib', '-C', cfgfile, '-Ln', 'main.vice', - //'--dbgfile', 'main.dbg', // TODO: get proper line numbers + '--dbgfile', 'main.dbg', '-o', 'main', '-m', 'main.map'].concat(step.args, libargs); // //#symbol ld (symbols not already merged into libargs) and //#flag ld @@ -210,7 +211,6 @@ export function linkLD65(step: BuildStep): BuildStepResult { newrom.set(aout.slice(0, aout.length - 0x2000), 0x2000); aout = newrom; } - //var dbgout = FS.readFile("main.dbg", {encoding:'utf8'}); putWorkFile("main", aout); putWorkFile("main.map", mapout); putWorkFile("main.vice", viceout); @@ -229,6 +229,16 @@ export function linkLD65(step: BuildStep): BuildStepResult { } } } + // symbol sizes from the linker debug file + var symbolsizes = {}; + try { + let dbgsyms = parseCC65DbgSizes(FS.readFile("main.dbg", { encoding: 'utf8' }), params.ignore_segments); + symbolsizes = dbgsyms.sizes; + // labels outside CPU address space (e.g. NES CHR) would alias real addresses + for (let name of dbgsyms.ignored) delete symbolmap[name]; + } catch (e) { + console.log("could not parse main.dbg", e); + } var segments = []; // TODO: CHR, banks, etc let re_seglist = /(\w+)\s+([0-9A-F]+)\s+([0-9A-F]+)\s+([0-9A-F]+)\s+([0-9A-F]+)/; @@ -237,6 +247,7 @@ export function linkLD65(step: BuildStep): BuildStepResult { for (let s of mapout.split('\n')) { if (parseseglist && (m = re_seglist.exec(s))) { let seg = m[1]; + if (params.ignore_segments?.includes(seg)) continue; let start = parseInt(m[2], 16); let size = parseInt(m[4], 16); let type = ''; @@ -280,6 +291,7 @@ export function linkLD65(step: BuildStep): BuildStepResult { listings: listings, errors: errors, symbolmap: symbolmap, + symbolsizes: symbolsizes, segments: segments }; } diff --git a/src/worker/tools/cc65dbg.ts b/src/worker/tools/cc65dbg.ts new file mode 100644 index 00000000..34deeee9 --- /dev/null +++ b/src/worker/tools/cc65dbg.ts @@ -0,0 +1,102 @@ +// Parser for the ld65 debug info file (--dbgfile). +// Each record is "\t=,..." e.g. +// seg id=2,name="BSS",start=0x00032A,size=0x0136,... +// span id=2146,seg=2,start=32,size=48 +// line id=3477,file=0,line=2273,span=2146 +// sym id=313,name="_attackers",...,val=0x34A,seg=2,type=lab + +type DbgRecord = { [key: string]: string }; + +function parseRecord(text: string): DbgRecord { + const rec: DbgRecord = {}; + const re = /(\w+)=("[^"]*"|[^,]*)/g; + let m; + while (m = re.exec(text)) { + rec[m[1]] = m[2].startsWith('"') ? m[2].slice(1, -1) : m[2]; + } + return rec; +} + +function num(s: string): number { + return s == null ? NaN : Number(s); // handles 0x prefix +} + +export interface CC65DbgSymbols { + sizes: { [sym: string]: number }; + ignored: string[]; // labels defined in ignored segments +} + +/** + * Returns the size in bytes of each label in the debug file. + * Uses the label's own size when ca65 records one (e.g. .proc scopes); + * otherwise sums the consecutive line spans that follow the label, up to + * the next label or a gap. Names defined more than once are omitted. + * Labels in ignoreSegments (e.g. NES CHR data) are listed in 'ignored'. + */ +export function parseCC65DbgSizes(dbg: string, ignoreSegments?: string[]): CC65DbgSymbols { + const segstart = new Map(); + const ignoredsegs = new Set(); + const ignored: string[] = []; + const spans = new Map(); + const linespans = new Set(); + const syms: DbgRecord[] = []; + for (const line of dbg.split('\n')) { + const tab = line.indexOf('\t'); + if (tab < 0) continue; + const type = line.substring(0, tab); + if (type != 'seg' && type != 'span' && type != 'line' && type != 'sym') continue; + const rec = parseRecord(line.substring(tab + 1)); + switch (type) { + case 'seg': + segstart.set(rec.id, num(rec.start)); + if (ignoreSegments?.includes(rec.name)) ignoredsegs.add(rec.id); + break; + case 'span': spans.set(rec.id, { seg: rec.seg, start: num(rec.start), size: num(rec.size) }); break; + case 'line': if (rec.span) rec.span.split('+').forEach(id => linespans.add(id)); break; + case 'sym': + if (rec.type != 'lab' || rec.seg == null || rec.val == null) break; + if (ignoredsegs.has(rec.seg)) ignored.push(rec.name); + else syms.push(rec); + break; + } + } + // span size by segment offset, from line spans only (scope spans cover whole ranges) + const spanat = new Map>(); + for (const [id, span] of spans) { + if (!linespans.has(id) || !(span.size > 0)) continue; + let segmap = spanat.get(span.seg); + if (!segmap) spanat.set(span.seg, segmap = new Map()); + segmap.set(span.start, Math.max(span.size, segmap.get(span.start) || 0)); + } + // label offsets by segment, to stop at the next label + const labelat = new Map>(); + for (const sym of syms) { + const ofs = num(sym.val) - segstart.get(sym.seg); + let set = labelat.get(sym.seg); + if (!set) labelat.set(sym.seg, set = new Set()); + set.add(ofs); + } + const sizes: { [sym: string]: number } = {}; + const dups = new Set(); + for (const sym of syms) { + if (sym.name in sizes) { dups.add(sym.name); continue; } + let size = num(sym.size); + if (isNaN(size)) { + const segmap = spanat.get(sym.seg); + const labels = labelat.get(sym.seg); + const ofs = num(sym.val) - segstart.get(sym.seg); + let cur = ofs; + let n: number; + while (segmap && (n = segmap.get(cur)) > 0) { + cur += n; + if (labels.has(cur)) break; + } + size = cur - ofs; + } + sizes[sym.name] = size; + } + for (const name of dups) delete sizes[name]; + // unknown sizes (no spans) are left out + for (const name in sizes) if (!(sizes[name] > 0)) delete sizes[name]; + return { sizes, ignored }; +} diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index 032ef973..bdab2dfd 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -415,6 +415,7 @@ describe('Platform Replay', () => { platform.nextFrame(); booted = platform.readAddress(0x0600) == 0x42; } + platform.pause(); assert.ok(booted, "ATR boot code did not run"); }); diff --git a/test/unit/testbuildparams.ts b/test/unit/testbuildparams.ts index 98e70468..fc5e7075 100644 --- a/test/unit/testbuildparams.ts +++ b/test/unit/testbuildparams.ts @@ -1,7 +1,8 @@ import assert from "assert"; import fs from "fs"; +import os from "os"; import path from "path"; -import { before, describe, it } from "mocha"; +import { after, before, describe, it } from "mocha"; import { store, fixParamsWithDefines, applyAsmProjectParams, parseBuildDirectives, applyBuildDirectives } from "../../src/worker/builder"; import { defineArgs, linkSymbolArgs, extraArgsFor, getPlatformToolConfig, TOOL_META } from "../../src/common/toolmeta"; import { compileSourceFile, preload } from "../../src/tools/testlib"; @@ -372,8 +373,9 @@ describe("applyAsmProjectParams (backwards compatibility)", function () { describe("build directive wiring (integration)", function () { this.timeout(120000); - const tmpdir = "/tmp/pi-agent/buildparams"; - before(function () { fs.mkdirSync(tmpdir, { recursive: true }); }); + let tmpdir: string; + before(function () { tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "buildparams-")); }); + after(function () { fs.rmSync(tmpdir, { recursive: true, force: true }); }); function writeSource(name: string, code: string): string { const p = path.join(tmpdir, name); diff --git a/test/unit/testcc65dbg.ts b/test/unit/testcc65dbg.ts new file mode 100644 index 00000000..95c6da92 --- /dev/null +++ b/test/unit/testcc65dbg.ts @@ -0,0 +1,68 @@ +import assert from "assert"; +import { describe, it } from "mocha"; +import { parseCC65DbgSizes } from "../../src/worker/tools/cc65dbg"; + +const DBG = [ + 'version\tmajor=2,minor=0', + 'seg\tid=0,name="CODE",start=0x008500,size=0x0100,addrsize=absolute,type=ro', + 'seg\tid=2,name="BSS",start=0x00032A,size=0x0136,addrsize=absolute,type=rw', + 'seg\tid=3,name="DATA",start=0x000300,size=0x0010,addrsize=absolute,type=rw', + 'seg\tid=8,name="CHARS",start=0x000000,size=0x2000,addrsize=absolute,type=rw', + 'span\tid=20,seg=8,start=0,size=4096', + 'span\tid=0,seg=2,start=0,size=32', + 'span\tid=1,seg=2,start=32,size=48', + 'span\tid=2,seg=2,start=80,size=1', + 'span\tid=3,seg=3,start=0,size=4', + 'span\tid=4,seg=3,start=4,size=4', + 'span\tid=5,seg=3,start=8,size=2', + 'span\tid=9,seg=2,start=0,size=300', // scope span: ignored + 'line\tid=0,file=0,line=10,span=0', + 'line\tid=1,file=0,line=12,span=1', + 'line\tid=2,file=0,line=14,span=2', + 'line\tid=3,file=0,line=20,span=3+4', + 'line\tid=4,file=0,line=21,span=5', + 'line\tid=20,file=1,line=1,span=20', + 'scope\tid=0,name="",mod=0,size=300,span=9', + 'sym\tid=0,name="_formation",addrsize=absolute,scope=0,def=0,val=0x32A,seg=2,type=lab', + 'sym\tid=1,name="_attackers",addrsize=absolute,scope=0,def=1,val=0x34A,seg=2,type=lab', + 'sym\tid=2,name="_last",addrsize=absolute,scope=0,def=2,val=0x37A,seg=2,type=lab', + 'sym\tid=3,name="_table",addrsize=absolute,scope=0,def=3,val=0x300,seg=3,type=lab', + 'sym\tid=4,name="_next",addrsize=absolute,scope=0,def=4,val=0x308,seg=3,type=lab', + 'sym\tid=5,name="_main",addrsize=absolute,size=41,scope=0,def=5,val=0x8500,seg=0,type=lab', + 'sym\tid=6,name="L0002",addrsize=absolute,size=2,scope=1,def=6,val=0x8510,seg=0,type=lab', + 'sym\tid=7,name="L0002",addrsize=absolute,size=3,scope=2,def=7,val=0x8520,seg=0,type=lab', + 'sym\tid=8,name="_nobytes",addrsize=absolute,scope=0,def=8,val=0x8600,seg=0,type=lab', + 'sym\tid=10,name="_chr_tiles",addrsize=absolute,scope=0,def=20,val=0x0,seg=8,type=lab', + 'sym\tid=9,name="FOO",addrsize=zeropage,scope=0,def=9,val=0x10,type=equ', +].join('\n'); + +describe('cc65 debug file', function () { + const { sizes, ignored } = parseCC65DbgSizes(DBG, ['HEADER', 'CHARS']); + + it('sizes labels from the line spans that follow them', function () { + assert.equal(sizes._formation, 32); + assert.equal(sizes._attackers, 48); + assert.equal(sizes._last, 1); + }); + + it('sums multiple spans up to the next label', function () { + assert.equal(sizes._table, 8); + assert.equal(sizes._next, 2); + }); + + it('uses sizes recorded by ca65', function () { + assert.equal(sizes._main, 41); + }); + + it('omits duplicates, equates and labels without spans', function () { + assert.ok(!('L0002' in sizes)); + assert.ok(!('FOO' in sizes)); + assert.ok(!('_nobytes' in sizes)); + }); + + it('reports labels in ignored segments instead of sizing them', function () { + assert.deepEqual(ignored, ['_chr_tiles']); + assert.ok(!('_chr_tiles' in sizes)); + assert.equal(parseCC65DbgSizes(DBG).sizes._chr_tiles, 4096); + }); +}); diff --git a/test/unit/testmemmaplayout.ts b/test/unit/testmemmaplayout.ts index 912bdd44..86c53757 100644 --- a/test/unit/testmemmaplayout.ts +++ b/test/unit/testmemmaplayout.ts @@ -70,7 +70,7 @@ describe('Memory map layout', function () { _small: 0x300, _buffer: 0x302, _last: 0x380, __BSS_RUN__: 0x300, _code: 0x8000, _zp: 0x10, }; - const vars = findLargeVariables(symbols, native.concat(linker)); + const vars = findLargeVariables(symbols, native.concat(linker), {minVarSize:16}); assert.deepEqual(vars.map(v => [v.name, v.start, v.end]), [ ['_last', 0x380, 0x400], ['_buffer', 0x302, 0x380], @@ -79,14 +79,14 @@ describe('Memory map layout', function () { }); it('skips the last symbol of a native-only RAM segment', function () { - const vars = findLargeVariables({ _a: 0x100, _b: 0x200 }, native); + const vars = findLargeVariables({ _a: 0x100, _b: 0x200 }, native, {minVarSize:16}); assert.deepEqual(vars.map(v => v.name), ['_a']); }); it('uses reported symbol sizes over gap estimates', function () { const symbols = { _a: 0x300, _b: 0x340, ZeroStart: 0x380, _c: 0x380 }; const sizes = { _a: 20, _b: 8, ZeroStart: 0, _c: 0x40 }; - const vars = findLargeVariables(symbols, native.concat(linker), {}, sizes); + const vars = findLargeVariables(symbols, native.concat(linker), {minVarSize:16}, sizes); assert.deepEqual(vars.map(v => [v.name, v.start, v.end, !!v.approx]), [ ['_c', 0x380, 0x3c0, false], ['_a', 0x300, 0x314, false],