From bb09a1ec33bf6ee9124860b3d02defac99616052 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Thu, 11 Mar 2021 22:03:05 -0800 Subject: [PATCH] Improve debug performance, abstract debugger. (#61) Stop stringifying opcodes during runtime and only do so upon inspection. Moves all the debugging logic to a common place to allow building an interface. --- .eslintrc.json | 1 + js/apple2.ts | 37 +++-- js/cpu6502.ts | 275 ++++++++++--------------------------- js/debugger.ts | 284 +++++++++++++++++++++++++++++++++++++++ js/main2.js | 2 +- js/main2e.js | 2 +- js/mmu.ts | 10 +- test/cpu6502.spec.js | 122 +---------------- test/js/debugger.spec.ts | 59 ++++++++ test/util/asserts.ts | 6 + test/util/bios.ts | 62 +++++++++ test/util/memory.ts | 37 +++++ 12 files changed, 554 insertions(+), 343 deletions(-) create mode 100644 js/debugger.ts create mode 100644 test/js/debugger.spec.ts create mode 100644 test/util/asserts.ts create mode 100644 test/util/bios.ts create mode 100644 test/util/memory.ts diff --git a/.eslintrc.json b/.eslintrc.json index 045bcb8..2758823 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -41,6 +41,7 @@ ], "no-redeclare": "off", "@typescript-eslint/no-redeclare": ["error"], + "no-dupe-class-members": "off", "no-console": [ "error", { diff --git a/js/apple2.ts b/js/apple2.ts index 8a2072a..2cb79bd 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -20,9 +20,10 @@ import { Apple2IOState } from './apple2io'; import CPU6502, { CpuState } from './cpu6502'; import MMU, { MMUState } from './mmu'; import RAM, { RAMState } from './ram'; -import { debug } from './util'; import SYMBOLS from './symbols'; +import Debugger, { DebuggerContainer } from './debugger'; + import { Restorable, rom } from './types'; import { processGamepad } from './ui/gamepad'; @@ -44,13 +45,10 @@ interface State { ram?: RAMState[], } -export class Apple2 implements Restorable { +export class Apple2 implements Restorable, DebuggerContainer { private paused = false; - private DEBUG = false; - private TRACE = false; - private MAX_TRACE = 256; - private trace: string[] = []; + private theDebugger?: Debugger; private runTimer: number | null = null; private runAnimationFrame: number | null = null; @@ -120,6 +118,9 @@ export class Apple2 implements Restorable { return; // already running } + this.theDebugger = new Debugger(this); + this.theDebugger.addSymbols(SYMBOLS); + const interval = 30; let now, last = Date.now(); @@ -134,19 +135,8 @@ export class Apple2 implements Restorable { step = stepMax; } - if (this.DEBUG) { - this.cpu.stepCyclesDebug(this.TRACE ? 1 : step, () => { - const line = this.cpu.dumpRegisters() + ' ' + - this.cpu.dumpPC(undefined, SYMBOLS); - if (this.TRACE) { - debug(line); - } else { - this.trace.push(line); - if (this.trace.length > this.MAX_TRACE) { - this.trace.shift(); - } - } - }); + if (this.theDebugger) { + this.theDebugger.stepCycles(step); } else { this.cpu.stepCycles(step); } @@ -234,7 +224,16 @@ export class Apple2 implements Restorable { return this.io; } + getMMU() { + return this.mmu; + } + + getVideoModes() { return this.vm; } + + getDebugger() { + return this.theDebugger; + } } diff --git a/js/cpu6502.ts b/js/cpu6502.ts index dcfd914..9170c3d 100644 --- a/js/cpu6502.ts +++ b/js/cpu6502.ts @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 Will Scullin + * Copyright 2010-2021 Will Scullin * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -13,8 +13,6 @@ import { Memory, MemoryPages, byte, word } from './types'; import { debug, toHex } from './util'; -type symbols = { [key: number]: string }; - export interface CpuOptions { '65C02'?: boolean; } @@ -29,7 +27,7 @@ export interface CpuState { cycles: number } -type Mode = +export type Mode = 'accumulator' | // A (Accumulator) 'implied' | // Implied 'immediate' | // # Immediate @@ -47,10 +45,10 @@ type Mode = 'absoluteXIndirect' | // (a, X), 'zeroPage_relative'; // zp, Relative -type Modes = Record; +export type Modes = Record; /** Addressing mode name to instruction size mapping. */ -const sizes: Modes = { +export const sizes: Modes = { accumulator: 1, implied: 1, immediate: 2, @@ -74,10 +72,20 @@ const sizes: Modes = { }; /** Status register flag numbers. */ -type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01; +export type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01; + +export type DebugInfo = { + pc: word, + ar: byte, + xr: byte, + yr: byte, + sr: byte, + sp: byte, + cmd: byte[], +}; /** Flags to status byte mask. */ -const flags: { [key: string]: flag } = { +export const flags: { [key: string]: flag } = { N: 0x80, // Negative V: 0x40, // oVerflow B: 0x10, // Break @@ -141,12 +149,12 @@ export default class CPU6502 { private readonly is65C02: boolean; /* Registers */ - private pc = 0; // Program Counter - private sr = 0x20; // Process Status Register - private ar = 0; // Accumulator - private xr = 0; // X Register - private yr = 0; // Y Register - private sp = 0xff; // Stack Pointer + private pc: word = 0; // Program Counter + private sr: byte = 0x20; // Process Status Register + private ar: byte = 0; // Accumulator + private xr: byte = 0; // X Register + private yr: byte = 0; // Y Register + private sp: byte = 0xff; // Stack Pointer private memPages: Memory[] = new Array(0x100); private resetHandlers: ResettablePageHandler[] = []; @@ -294,10 +302,6 @@ export default class CPU6502 { return this.readByte(addr) | (this.readByte(addr + 1) << 8); } - private readWordDebug(addr: word): word { - return this.readByteDebug(addr) | (this.readByteDebug(addr + 1) << 8); - } - private readWordPC(): word { return this.readBytePC() | (this.readBytePC() << 8); } @@ -991,105 +995,24 @@ export default class CPU6502 { return unk; } - private dumpArgs(addr: word, m: Mode, symbols: symbols) { - function toHexOrSymbol(v: word, n?: number) { - if (symbols && symbols[v]) { - return symbols[v]; - } else { - return '$' + toHex(v, n); - } - } - let off, val; - let result = ''; - switch (m) { - case 'implied': - break; - case 'immediate': - result = '#' + toHexOrSymbol(this.readByteDebug(addr)); - break; - case 'absolute': - result = '' + toHexOrSymbol(this.readWordDebug(addr), 4); - break; - case 'zeroPage': - result = '' + toHexOrSymbol(this.readByteDebug(addr)); - break; - case 'relative': - { - let off = this.readByteDebug(addr); - if (off > 127) { - off -= 256; - } - addr += off + 1; - result = '' + toHexOrSymbol(addr, 4) + ' (' + off + ')'; - } - break; - case 'absoluteX': - result = '' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',X'; - break; - case 'absoluteY': - result = '' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',Y'; - break; - case 'zeroPageX': - result = '' + toHexOrSymbol(this.readByteDebug(addr)) + ',X'; - break; - case 'zeroPageY': - result = '' + toHexOrSymbol(this.readByteDebug(addr)) + ',Y'; - break; - case 'absoluteIndirect': - result = '(' + toHexOrSymbol(this.readWordDebug(addr), 4) + ')'; - break; - case 'zeroPageXIndirect': - result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + ',X)'; - break; - case 'zeroPageIndirectY': - result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + '),Y'; - break; - case 'accumulator': - result = 'A'; - break; - case 'zeroPageIndirect': - result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + ')'; - break; - case 'absoluteXIndirect': - result = '(' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',X)'; - break; - case 'zeroPage_relative': - val = this.readByteDebug(addr); - off = this.readByteDebug(addr + 1); - if (off > 127) { - off -= 256; - } - addr += off + 2; - result = '' + toHexOrSymbol(val) + ',' + toHexOrSymbol(addr, 4) + ' (' + off + ')'; - break; - default: - break; - } - return result; - } - - public step(cb: callback) { + public step(cb?: callback) { this.sync = true; const op = this.opary[this.readBytePC()]; this.sync = false; op.op(op.modeFn); - if (cb) { - cb(this); - } + cb?.(this); } - public stepDebug(n: number, cb: callback) { + public stepN(n: number, cb?: callback) { for (let idx = 0; idx < n; idx++) { this.sync = true; const op = this.opary[this.readBytePC()]; this.sync = false; op.op(op.modeFn); - if (cb) { - cb(this); - } + cb?.(this); } } @@ -1104,7 +1027,7 @@ export default class CPU6502 { } } - public stepCyclesDebug(c: number, cb: callback): void { + public stepCyclesDebug(c: number, cb?: callback): void { const end = this.cycles + c; while (this.cycles < end) { @@ -1113,9 +1036,7 @@ export default class CPU6502 { this.sync = false; op.op(op.modeFn); - if (cb) { - cb(this); - } + cb?.(this); } } @@ -1173,83 +1094,26 @@ export default class CPU6502 { this.pc = pc; } - public dumpPC(pc: word | undefined, symbols: symbols) { - if (pc === undefined) { - pc = this.pc; - } - const b = this.readByteDebug(pc), - op = this.opary[b], - size = sizes[op.mode]; - let result = toHex(pc, 4) + '- '; - - if (symbols) { - if (symbols[pc]) { - result += symbols[pc] + - ' '.substring(symbols[pc].length); - } else { - result += ' '; - } + public getDebugInfo(): DebugInfo { + const b = this.readByteDebug(this.pc); + const op = this.opary[b]; + const size = sizes[op.mode]; + const cmd = new Array(size); + cmd[0] = b; + for (let idx = 1; idx < size; idx++) { + cmd[idx] = this.readByteDebug(this.pc + idx); } - for (let idx = 0; idx < 4; idx++) { - if (idx < size) { - result += toHex(this.readByteDebug(pc + idx)) + ' '; - } else { - result += ' '; - } - } - - if (op === undefined) - result += '??? (' + toHex(b) + ')'; - else - result += op.name + ' ' + this.dumpArgs(pc + 1, op.mode, symbols); - - return result; + return { + pc: this.pc, + ar: this.ar, + xr: this.xr, + yr: this.yr, + sr: this.sr, + sp: this.sp, + cmd + }; } - - public dumpPage(start?: word, end?: word) { - let result = ''; - if (start === undefined) { - start = this.pc >> 8; - } - if (end === undefined) { - end = start; - } - for (let page = start; page <= end; page++) { - for (let idx = 0; idx < 16; idx++) { - result += toHex(page) + toHex(idx << 4) + ': '; - for (let jdx = 0; jdx < 16; jdx++) { - const b = this.readByteDebug(page * 256 + idx * 16 + jdx); - result += toHex(b) + ' '; - } - result += ' '; - for (let jdx = 0; jdx < 16; jdx++) { - const b = this.readByte(page * 256 + idx * 16 + jdx) & 0x7f; - if (b >= 0x20 && b < 0x7f) { - result += String.fromCharCode(b); - } else { - result += '.'; - } - } - result += '\n'; - } - } - return result; - } - - public list(_pc: word, symbols: symbols) { - if (_pc === undefined) { - _pc = this.pc; - } - const results = []; - for (let jdx = 0; jdx < 20; jdx++) { - const b = this.readByte(_pc), op = this.opary[b]; - results.push(this.dumpPC(_pc, symbols)); - _pc += sizes[op.mode]; - } - return results; - } - public getSync() { return this.sync; } @@ -1258,8 +1122,8 @@ export default class CPU6502 { return this.cycles; } - public registers() { - return [this.pc, this.ar, this.xr, this.yr, this.sr, this.sp]; + public getOpInfo(opcode: byte) { + return this.opary[opcode]; } public getState(): CpuState { @@ -1284,29 +1148,36 @@ export default class CPU6502 { this.cycles = state.cycles; } - public dumpRegisters() { - return toHex(this.pc, 4) + - '- A=' + toHex(this.ar) + - ' X=' + toHex(this.xr) + - ' Y=' + toHex(this.yr) + - ' P=' + toHex(this.sr) + - ' S=' + toHex(this.sp) + - ' ' + - ((this.sr & flags.N) ? 'N' : '-') + - ((this.sr & flags.V) ? 'V' : '-') + - '-' + - ((this.sr & flags.B) ? 'B' : '-') + - ((this.sr & flags.D) ? 'D' : '-') + - ((this.sr & flags.I) ? 'I' : '-') + - ((this.sr & flags.Z) ? 'Z' : '-') + - ((this.sr & flags.C) ? 'C' : '-'); - } + public read(addr: word): byte; + public read(page: byte, off: byte): byte; - public read(page: byte, off: byte): byte { + public read(a: number, b?: number): byte { + let page, off; + if (b !== undefined) { + page = a; + off = b; + } else { + page = a >> 8; + off = a & 0xff; + } return this.memPages[page].read(page, off); } - public write(page: byte, off: byte, val: byte) { + public write(addr: word, val: byte): void; + public write(page: byte, off: byte, val: byte): void; + + public write(a: number, b: number, c?: byte): void { + let page, off, val; + + if (c !== undefined ) { + page = a; + off = b; + val = c; + } else { + page = a >> 8; + off = a & 0xff; + val = b; + } this.memPages[page].write(page, off, val); } diff --git a/js/debugger.ts b/js/debugger.ts new file mode 100644 index 0000000..558f6d9 --- /dev/null +++ b/js/debugger.ts @@ -0,0 +1,284 @@ +import { debug, toHex } from './util'; +import { byte, word } from './types'; + +import CPU6502, { DebugInfo, flags, sizes } from './cpu6502'; + +export interface DebuggerContainer { + run: () => void; + stop: () => void; + getCPU: () => CPU6502; +} + +type symbols = { [key: number]: string }; +type breakpointFn = (info: DebugInfo) => boolean + +const alwaysBreak = (_info: DebugInfo) => { return true; }; + +export default class Debugger { + private cpu: CPU6502; + private verbose = false; + private maxTrace = 256; + private trace: DebugInfo[] = []; + private breakpoints: Map = new Map(); + private symbols: symbols = {}; + + constructor(private container: DebuggerContainer) { + this.cpu = container.getCPU(); + } + + stepCycles(cycles: number) { + this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => { + const info = this.cpu.getDebugInfo(); + + if (this.breakpoints.get(info.pc)?.(info)) { + debug('breakpoint', this.printDebugInfo(info)); + this.container.stop(); + return; + } + if (this.verbose) { + debug(this.printDebugInfo(info)); + } else { + this.trace.push(info); + if (this.trace.length > this.maxTrace) { + this.trace.shift(); + } + } + }); + } + + continue() { + this.container.run(); + } + + setVerbose(verbose: boolean) { + this.verbose = verbose; + } + + setMaxTrace(maxTrace: number) { + this.maxTrace = maxTrace; + } + + getTrace() { + return this.trace.map(this.printDebugInfo).join('\n'); + } + + printTrace() { + debug(this.getTrace()); + } + + setBreakpoint(addr: word, exp?: breakpointFn) { + this.breakpoints.set(addr, exp || alwaysBreak); + } + + clearBreakpoint(addr: word) { + this.breakpoints.delete(addr); + } + + listBreakpoints() { + for(const [addr, fn] of this.breakpoints.entries()) { + debug(toHex(addr, 4), fn); + } + } + + addSymbols(symbols: symbols) { + this.symbols = { ...this.symbols, ...symbols }; + } + + printDebugInfo(info: DebugInfo) { + const { pc, cmd } = info; + const symbol = this.padWithSymbol(pc); + + return [ + toHex(pc, 4), + '- ', symbol, + this.dumpRegisters(info), + ' ', + this.dumpRawOp(cmd), + ' ', + this.dumpOp(pc, cmd) + ].join(''); + } + + dumpPC(pc: word) { + const b = this.cpu.read(pc); + const op = this.cpu.getOpInfo(b); + const size = sizes[op.mode]; + let result = toHex(pc, 4) + '- '; + + result += this.padWithSymbol(pc); + + const cmd = new Array(size); + for (let idx = 0, jdx = pc; idx < size; idx++, jdx++) { + cmd[idx] = this.cpu.read(jdx); + } + + result += this.dumpRawOp(cmd) + ' ' + this.dumpOp(pc, cmd); + + return result; + } + + dumpRegisters(debugInfo?: DebugInfo) { + if (debugInfo === undefined) { + debugInfo = this.cpu.getDebugInfo(); + } + const { pc, ar, xr, yr, sr, sp } = debugInfo; + return [ + toHex(pc, 4), + '- ', + ' A=' + toHex(ar), + ' X=' + toHex(xr), + ' Y=' + toHex(yr), + ' P=' + toHex(sr), + ' S=' + toHex(sp), + ' ', + ((sr & flags.N) ? 'N' : '-'), + ((sr & flags.V) ? 'V' : '-'), + '-', + ((sr & flags.B) ? 'B' : '-'), + ((sr & flags.D) ? 'D' : '-'), + ((sr & flags.I) ? 'I' : '-'), + ((sr & flags.Z) ? 'Z' : '-'), + ((sr & flags.C) ? 'C' : '-') + ].join(''); + } + + dumpPage(start: byte, end?: byte) { + let result = ''; + if (end === undefined) { + end = start; + } + for (let page = start; page <= end; page++) { + for (let idx = 0; idx < 16; idx++) { + result += toHex(page) + toHex(idx << 4) + ': '; + for (let jdx = 0; jdx < 16; jdx++) { + const b = this.cpu.read(page, idx * 16 + jdx); + result += toHex(b) + ' '; + } + result += ' '; + for (let jdx = 0; jdx < 16; jdx++) { + const b = this.cpu.read(page, idx * 16 + jdx) & 0x7f; + if (b >= 0x20 && b < 0x7f) { + result += String.fromCharCode(b); + } else { + result += '.'; + } + } + result += '\n'; + } + } + return result; + } + + list(pc: word) { + const results = []; + for (let idx = 0; idx < 20; idx++) { + const b = this.cpu.read(pc); + const op = this.cpu.getOpInfo(b); + results.push(this.dumpPC(pc)); + pc += sizes[op.mode]; + } + return results; + } + + private padWithSymbol(pc: word): string { + const padding = ' '; + const symbol = this.symbols[pc]; + + let result: string = padding; + if (symbol) { + result = `${symbol}${padding.substring(symbol.length)}`; + } + return result; + } + + private dumpRawOp(parts: byte[]) { + const result = new Array(4); + for (let idx = 0; idx < 4; idx++) { + if (idx < parts.length) { + result[idx] = toHex(parts[idx]); + } else { + result[idx] = ' '; + } + } + return result.join(' '); + } + + private dumpOp(pc: word, parts: byte[]) { + const op = this.cpu.getOpInfo(parts[0]); + const lsb = parts[1]; + const msb = parts[2]; + const addr = (msb << 8) | lsb; + let val; + let off; + const toHexOrSymbol = (v: word, n?: number) => ( + this.symbols[v] || ('$' + toHex(v, n)) + ); + + let result = op.name + ' '; + switch (op.mode) { + case 'implied': + break; + case 'immediate': + result += '#' + toHexOrSymbol(lsb); + break; + case 'absolute': + result += '' + toHexOrSymbol(addr, 4); + break; + case 'zeroPage': + result += '' + toHexOrSymbol(lsb); + break; + case 'relative': + { + off = lsb; + if (off > 127) { + off -= 256; + } + pc += off + 1; + result += '' + toHexOrSymbol(pc, 4) + ' (' + off + ')'; + } + break; + case 'absoluteX': + result += '' + toHexOrSymbol(addr, 4)+ ',X'; + break; + case 'absoluteY': + result += '' + toHexOrSymbol(addr, 4) + ',Y'; + break; + case 'zeroPageX': + result += '' + toHexOrSymbol(lsb) + ',X'; + break; + case 'zeroPageY': + result += '' + toHexOrSymbol(lsb) + ',Y'; + break; + case 'absoluteIndirect': + result += '(' + toHexOrSymbol(addr, 4) + ')'; + break; + case 'zeroPageXIndirect': + result += '(' + toHexOrSymbol(lsb) + ',X)'; + break; + case 'zeroPageIndirectY': + result += '(' + toHexOrSymbol(lsb) + '),Y'; + break; + case 'accumulator': + result += 'A'; + break; + case 'zeroPageIndirect': + result += '(' + toHexOrSymbol(lsb) + ')'; + break; + case 'absoluteXIndirect': + result += '(' + toHexOrSymbol(addr, 4) + ',X)'; + break; + case 'zeroPage_relative': + val = lsb; + off = msb; + if (off > 127) { + off -= 256; + } + pc += off + 2; + result += '' + toHexOrSymbol(val) + ',' + toHexOrSymbol(pc, 4) + ' (' + off + ')'; + break; + default: + break; + } + return result; + } +} diff --git a/js/main2.js b/js/main2.js index 9c35c5d..6932a2b 100644 --- a/js/main2.js +++ b/js/main2.js @@ -70,7 +70,7 @@ var options = { tick: updateUI }; -var apple2 = new Apple2(options); +export var apple2 = new Apple2(options); var cpu = apple2.getCPU(); var io = apple2.getIO(); diff --git a/js/main2e.js b/js/main2e.js index 7288efa..131da75 100644 --- a/js/main2e.js +++ b/js/main2e.js @@ -52,7 +52,7 @@ var options = { tick: updateUI }; -var apple2 = new Apple2(options); +export var apple2 = new Apple2(options); var io = apple2.getIO(); var cpu = apple2.getCPU(); diff --git a/js/mmu.ts b/js/mmu.ts index 1cfb141..f26ae83 100644 --- a/js/mmu.ts +++ b/js/mmu.ts @@ -172,9 +172,9 @@ export interface MMUState { } export default class MMU implements Memory, Restorable { - private _readPages = new Array(0x100); - private _writePages = new Array(0x100); - private _pages = new Array(0x100); + private _readPages = new Array(0x100); + private _writePages = new Array(0x100); + private _pages = new Array(0x100); // Language Card RAM Softswitches private _bank1: boolean; @@ -833,6 +833,10 @@ export default class MMU implements Memory, Restorable { this._writePages[page].write(page, off, val); } + public writeBank(bank: number,page: byte, off: byte, val: byte) { + this._pages[page][bank].write(page, off, val); + } + public resetVB() { this._vbEnd = this.cpu.getCycles() + 1000; } diff --git a/test/cpu6502.spec.js b/test/cpu6502.spec.js index a910481..8bd7a0e 100644 --- a/test/cpu6502.spec.js +++ b/test/cpu6502.spec.js @@ -1,96 +1,6 @@ import CPU6502 from '../js/cpu6502'; - -function assertByte(b) { - expect(b <= 0xFF).toEqual(true); - expect(b >= 0x00).toEqual(true); -} - -function Memory(size) { - var data = Buffer.alloc(size << 8); - - return { - start: function() { - return 0; - }, - - end: function() { - return size - 1; - }, - - read: function(page, off) { - assertByte(page); - assertByte(off); - - return data[(page << 8) | off]; - }, - - write: function(page, off, val) { - assertByte(page); - assertByte(off); - assertByte(val); - - data[(page << 8) | off] = val; - }, - - reset: function() { - } - }; -} - -function Program(page, code) { - var data = Buffer.from(code); - - return { - start: function() { - return page; - }, - - end: function() { - return page; - }, - - read: function(page, off) { - assertByte(page); - assertByte(off); - return data[off]; - } - }; -} - -var bios = new Program(0xff, [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x0D, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xff, 0x00, 0x04, 0x00, 0xff -]); +import { TestMemory } from './util/memory'; +import { bios, Program } from './util/bios'; var FLAGS = { N: 0x80, // Negative @@ -178,14 +88,14 @@ function testCode(code, steps, setupState, expectedState) { cpu.addPageHandler(program); cpu.setState(initialState); - cpu.stepDebug(steps); + cpu.stepN(steps); expectState(initialState, finalState); } describe('CPU6502', function() { beforeEach(function() { cpu = new CPU6502(); - memory = new Memory(4); + memory = new TestMemory(4); cpu.addPageHandler(memory); cpu.addPageHandler(bios); @@ -1516,34 +1426,12 @@ describe('CPU6502', function() { }); }); }); - - describe('#utility', function() { - it('should list', function() { - var listing = cpu.list(0xff00); - expect(listing[0]).toEqual('FF00- 00 00 BRK #$00'); - }); - - it('should list with symbols', function() { - var listing = cpu.list(0xff00, {0x00: 'ZERO', 0xFF00: 'ENTRY'}); - expect(listing[0]).toEqual('FF00- ENTRY 00 00 BRK #ZERO'); - }); - - it('should dump page', function() { - var page = cpu.dumpPage(0xff); - expect(page).toContain('FF80: 48 45 4C 4C 4F 0D 00 00 00 00 00 00 00 00 00 00 HELLO...........'); - }); - - it('should dump registers', function() { - var regs = cpu.dumpRegisters(); - expect(regs).toEqual('0000- A=00 X=00 Y=00 P=20 S=FF --------'); - }); - }); }); describe('65c02', function() { beforeEach(function() { cpu = new CPU6502({'65C02': true}); - memory = new Memory(4); + memory = new TestMemory(4); cpu.addPageHandler(memory); cpu.addPageHandler(bios); diff --git a/test/js/debugger.spec.ts b/test/js/debugger.spec.ts new file mode 100644 index 0000000..2f2f76f --- /dev/null +++ b/test/js/debugger.spec.ts @@ -0,0 +1,59 @@ +import CPU6502 from '../../js/cpu6502'; +import Debugger, { DebuggerContainer } from '../../js/debugger'; +import { MemoryPages } from '../../js/types'; +import { TestMemory } from '../util/memory'; +import { bios } from '../util/bios'; + +describe('Debugger', () => { + let cpu: CPU6502; + let debuggerContainer: DebuggerContainer; + let theDebugger: Debugger; + let memory: MemoryPages; + + beforeEach(() => { + cpu = new CPU6502(); + memory = new TestMemory(4); + + cpu.addPageHandler(memory); + cpu.addPageHandler(bios); + + debuggerContainer = { + getCPU: () => cpu, + run: jest.fn(), + stop: jest.fn(), + }; + theDebugger = new Debugger(debuggerContainer); + }); + + describe('#utility', () => { + it('should list without symbols', () => { + const listing = theDebugger.list(0xff00); + expect(listing[0]).toEqual( + 'FF00- 00 00 BRK #$00' + ); + }); + + it('should list with symbols', () => { + theDebugger.addSymbols({0x00: 'ZERO', 0xFF00: 'ENTRY'}); + + const listing = theDebugger.list(0xff00); + expect(listing[0]).toEqual( + 'FF00- ENTRY 00 00 BRK #ZERO' + ); + }); + + it('should dump page', () => { + const page = theDebugger.dumpPage(0xff); + expect(page).toContain( + 'FF80: 48 45 4C 4C 4F 0D 00 00 00 00 00 00 00 00 00 00 HELLO...........' + ); + }); + + it('should dump registers', () => { + const regs = theDebugger.dumpRegisters(); + expect(regs).toEqual( + '0000- A=00 X=00 Y=00 P=20 S=FF --------' + ); + }); + }); +}); diff --git a/test/util/asserts.ts b/test/util/asserts.ts new file mode 100644 index 0000000..46023cb --- /dev/null +++ b/test/util/asserts.ts @@ -0,0 +1,6 @@ +import { byte } from '../../js/types'; + +export const assertByte = (b: byte) => { + expect(b <= 0xFF).toEqual(true); + expect(b >= 0x00).toEqual(true); +}; diff --git a/test/util/bios.ts b/test/util/bios.ts new file mode 100644 index 0000000..de3ad3b --- /dev/null +++ b/test/util/bios.ts @@ -0,0 +1,62 @@ +import { MemoryPages, byte } from '../../js/types'; +import { assertByte } from './asserts'; + +export class Program implements MemoryPages { + private data: Buffer; + + constructor(private page: byte, code: byte[]) { + this.data = Buffer.from(code); + } + + start() { + return this.page; + } + + end() { + return this.page; + } + + read(page: byte, off: byte) { + assertByte(page); + assertByte(off); + return this.data[off]; + } + + write(_page: byte, _off: byte, _val: byte) { + } +} + +export const bios = new Program(0xff, [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x0D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x04, 0x00, 0xff +]); diff --git a/test/util/memory.ts b/test/util/memory.ts new file mode 100644 index 0000000..3c67921 --- /dev/null +++ b/test/util/memory.ts @@ -0,0 +1,37 @@ +import { MemoryPages, byte } from '../../js/types'; +import { assertByte } from './asserts'; + +export class TestMemory implements MemoryPages { + private data: Buffer; + + constructor(private size: number) { + this.data = Buffer.alloc(size << 8); + } + + start() { + return 0; + } + + end() { + return this.size - 1; + } + + read(page: byte, off: byte) { + assertByte(page); + assertByte(off); + + return this.data[(page << 8) | off]; + } + + write(page: byte, off: byte, val: byte) { + assertByte(page); + assertByte(off); + assertByte(val); + + this.data[(page << 8) | off] = val; + } + + reset() { + } +} +