From c0ff1e812921ca7232da8564acb90abf973dd664 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Wed, 13 Jul 2022 20:34:50 -0700 Subject: [PATCH] More debugger panels (#141) --- js/apple2.ts | 2 +- js/apple2io.ts | 4 + js/applesoft/compiler.ts | 6 +- js/applesoft/decompiler.ts | 73 +++- js/applesoft/heap.ts | 200 ++++++++++ js/applesoft/zeropage.ts | 6 + js/canvas.ts | 10 +- js/cards/langcard.ts | 58 +-- js/components/Apple2.tsx | 4 +- js/components/Debugger.tsx | 212 ---------- js/components/Keyboard.tsx | 3 + js/components/Tabs.tsx | 63 +++ js/components/css/Tabs.module.css | 23 ++ js/components/debugger/Applesoft.tsx | 137 +++++++ js/components/debugger/CPU.tsx | 211 ++++++++++ js/components/debugger/Debugger.tsx | 40 ++ js/components/debugger/Memory.tsx | 363 ++++++++++++++++++ js/components/debugger/VideoModes.tsx | 94 +++++ .../debugger/css/Applesoft.module.css | 48 +++ js/components/debugger/css/CPU.module.css | 35 ++ .../{ => debugger}/css/Debugger.module.css | 73 +--- js/components/debugger/css/Memory.module.css | 154 ++++++++ .../debugger/css/VideoModes.module.css | 10 + js/cpu6502.ts | 16 +- js/debugger.ts | 6 +- js/gl.ts | 8 + js/mmu.ts | 76 +++- js/ui/apple2.ts | 10 +- js/videomodes.ts | 2 + test/js/debugger.spec.ts | 3 +- 30 files changed, 1595 insertions(+), 355 deletions(-) create mode 100644 js/applesoft/heap.ts delete mode 100644 js/components/Debugger.tsx create mode 100644 js/components/Tabs.tsx create mode 100644 js/components/css/Tabs.module.css create mode 100644 js/components/debugger/Applesoft.tsx create mode 100644 js/components/debugger/CPU.tsx create mode 100644 js/components/debugger/Debugger.tsx create mode 100644 js/components/debugger/Memory.tsx create mode 100644 js/components/debugger/VideoModes.tsx create mode 100644 js/components/debugger/css/Applesoft.module.css create mode 100644 js/components/debugger/css/CPU.module.css rename js/components/{ => debugger}/css/Debugger.module.css (61%) create mode 100644 js/components/debugger/css/Memory.module.css create mode 100644 js/components/debugger/css/VideoModes.module.css diff --git a/js/apple2.ts b/js/apple2.ts index fc1d26b..6da9c44 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -152,7 +152,7 @@ export class Apple2 implements Restorable, DebuggerContainer { return; // already running } - this.theDebugger = new Debugger(this); + this.theDebugger = new Debugger(this.cpu, this); this.theDebugger.addSymbols(SYMBOLS); const interval = 30; diff --git a/js/apple2io.ts b/js/apple2io.ts index eda59a5..316411c 100644 --- a/js/apple2io.ts +++ b/js/apple2io.ts @@ -412,6 +412,10 @@ export default class Apple2IO implements MemoryPages, Restorable this._slot[slot] = card; } + getSlot(slot: slot): Card | null { + return this._slot[slot]; + } + keyDown(ascii: byte) { this._keyDown = true; this._key = ascii | 0x80; diff --git a/js/applesoft/compiler.ts b/js/applesoft/compiler.ts index 7757673..6b7917c 100644 --- a/js/applesoft/compiler.ts +++ b/js/applesoft/compiler.ts @@ -124,13 +124,13 @@ export default class ApplesoftCompiler { private lines: Map = new Map(); /** - * Loads an AppleSoft BASIC program into memory. + * Loads an Applesoft BASIC program into memory. * * @param mem Memory, including zero page, into which the program is * loaded. * @param program A string with a BASIC program to compile (tokenize). * @param programStart Optional start address of the program. Defaults to - * standard AppleSoft program address, 0x801. + * standard Applesoft program address, 0x801. */ static compileToMemory(mem: Memory, program: string, programStart: word = PROGRAM_START) { const compiler = new ApplesoftCompiler(); @@ -179,7 +179,7 @@ export default class ApplesoftCompiler { for (const possibleToken in STRING_TO_TOKEN) { if (lineBuffer.lookingAtToken(possibleToken)) { // NOTE(flan): This special token-preference - // logic is straight from the AppleSoft BASIC + // logic is straight from the Applesoft BASIC // code (D5BE-D5CA in the Apple //e ROM). // Found a token diff --git a/js/applesoft/decompiler.ts b/js/applesoft/decompiler.ts index 37260fb..d09ccd1 100644 --- a/js/applesoft/decompiler.ts +++ b/js/applesoft/decompiler.ts @@ -1,4 +1,5 @@ import { byte, word, ReadonlyUint8Array, Memory } from '../types'; +import { toHex } from 'js/util'; import { TOKEN_TO_STRING, STRING_TO_TOKEN } from './tokens'; import { TXTTAB, PRGEND } from './zeropage'; @@ -8,6 +9,24 @@ const LETTERS = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' + '`abcdefghijklmnopqrstuvwxyz{|}~ '; +/** + * Resolves a token value to a token string or character. + * + * @param token + * @returns string representing token + */ +const resolveToken = (token: byte) => { + let tokenString; + if (token >= 0x80 && token <= 0xea) { + tokenString = TOKEN_TO_STRING[token]; + } else if (LETTERS[token] !== undefined) { + tokenString = LETTERS[token]; + } else { + tokenString = `[${toHex(token)}]`; + } + return tokenString; +}; + interface ListOptions { apple2: 'e' | 'plus'; columns: number; // usually 40 or 80 @@ -26,6 +45,8 @@ const DEFAULT_DECOMPILE_OPTIONS: DecompileOptions = { style: 'pretty', }; +const MAX_LINES = 32768; + export default class ApplesoftDecompiler { /** @@ -38,6 +59,9 @@ export default class ApplesoftDecompiler { const start = ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8); const end = ram.read(0x00, PRGEND) + (ram.read(0x00, PRGEND + 1) << 8); + if (start >= 0xc000 || end >= 0xc000) { + throw new Error(`Program memory ${toHex(start, 4)}-${toHex(end, 4)} out of range`); + } for (let addr = start; addr <= end; addr++) { program.push(ram.read(addr >> 8, addr & 0xff)); } @@ -73,18 +97,26 @@ export default class ApplesoftDecompiler { * @param callback A function to call for each line. The first parameter * is the offset of the line number of the line; the tokens follow. */ - private forEachLine(from: number, to: number, - callback: (offset: word) => void): void { - + private forEachLine( + from: number, to: number, + callback: (offset: word) => void): void + { + let count = 0; let offset = 0; let nextLineAddr = this.wordAt(offset); let nextLineNo = this.wordAt(offset + 2); while (nextLineAddr !== 0 && nextLineNo < from) { + if (++count > MAX_LINES) { + throw new Error('Loop detected in listing'); + } offset = nextLineAddr; nextLineAddr = this.wordAt(offset); nextLineNo = this.wordAt(offset + 2); } while (nextLineAddr !== 0 && nextLineNo <= to) { + if (++count > MAX_LINES) { + throw new Error('Loop detected in listing'); + } callback(offset + 2); offset = nextLineAddr - this.base; nextLineAddr = this.wordAt(offset); @@ -113,13 +145,17 @@ export default class ApplesoftDecompiler { // always assumes that there is space for one token—which would // have been the case on a realy Apple. while (this.program[offset] !== 0) { + if (offset >= this.program.length) { + lines.unshift('Unterminated line: '); + break; + } const token = this.program[offset]; if (token >= 0x80 && token <= 0xea) { line += ' '; // D750, always put a space in front of token - line += TOKEN_TO_STRING[token]; + line += resolveToken(token); line += ' '; // D762, always put a trailing space } else { - line += LETTERS[token]; + line += resolveToken(token); } offset++; @@ -194,17 +230,14 @@ export default class ApplesoftDecompiler { offset += 2; while (this.program[offset] !== 0) { - const token = this.program[offset]; - let tokenString: string; - if (token >= 0x80 && token <= 0xea) { - tokenString = TOKEN_TO_STRING[token]; - if (tokenString === 'PRINT') { - tokenString = '?'; - } - } else { - tokenString = LETTERS[token]; + if (offset >= this.program.length) { + return 'Unterminated line: ' + result; + } + const token = this.program[offset]; + let tokenString = resolveToken(token); + if (tokenString === 'PRINT') { + tokenString = '?'; } - if (spaceIf(tokenString)) { result += ' '; } @@ -239,13 +272,11 @@ export default class ApplesoftDecompiler { offset += 2; while (this.program[offset] !== 0) { - const token = this.program[offset]; - let tokenString: string; - if (token >= 0x80 && token <= 0xea) { - tokenString = TOKEN_TO_STRING[token]; - } else { - tokenString = LETTERS[token]; + if (offset >= this.program.length) { + return 'Unterminated line: ' + result; } + const token = this.program[offset]; + const tokenString = resolveToken(token); if (tokenString === '"') { inString = !inString; } diff --git a/js/applesoft/heap.ts b/js/applesoft/heap.ts new file mode 100644 index 0000000..6228e53 --- /dev/null +++ b/js/applesoft/heap.ts @@ -0,0 +1,200 @@ +import { byte, word, Memory } from 'js/types'; +import { toHex } from 'js/util'; +import { + CURLINE, + ARG, + FAC, + ARYTAB, + STREND, + TXTTAB, + VARTAB +} from './zeropage'; + +export type ApplesoftValue = word | number | string | ApplesoftArray; +export type ApplesoftArray = Array; + +export enum VariableType { + Float = 0, + String = 1, + Function = 2, + Integer = 3 +} + +export interface ApplesoftVariable { + name: string; + sizes?: number[]; + type: VariableType; + value: ApplesoftValue | undefined; +} + + +export class ApplesoftHeap { + constructor(private mem: Memory) {} + + private readByte(addr: word): byte { + const page = addr >> 8; + const off = addr & 0xff; + + if (page >= 0xc0) { + throw new Error(`Address ${toHex(page)} out of range`); + } + + return this.mem.read(page, off); + } + + private readWord(addr: word): word { + const lsb = this.readByte(addr); + const msb = this.readByte(addr + 1); + + return (msb << 8) | lsb; + } + + private readInt(addr: word): word { + const msb = this.readByte(addr); + const lsb = this.readByte(addr + 1); + + return (msb << 8) | lsb; + } + + private readFloat(addr: word, { unpacked } = { unpacked: false }): number { + let exponent = this.readByte(addr); + if (exponent === 0) { + return 0; + } + exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7F) - 1); + + let msb = this.readByte(addr + 1); + const sb3 = this.readByte(addr + 2); + const sb2 = this.readByte(addr + 3); + const lsb = this.readByte(addr + 4); + let sign; + if (unpacked) { + const sb = this.readByte(addr + 5); + sign = sb & 0x80 ? -1 : 1; + } else { + sign = msb & 0x80 ? -1 : 1; + } + msb &= 0x7F; + const mantissa = (msb << 24) | (sb3 << 16) | (sb2 << 8) | lsb; + + return sign * (1 + mantissa / 0x80000000) * Math.pow(2, exponent); + } + + private readString(len: byte, addr: word): string { + let str = ''; + for (let idx = 0; idx < len; idx++) { + str += String.fromCharCode(this.readByte(addr + idx) & 0x7F); + } + return str; + } + + private readVar(addr: word) { + const firstByte = this.readByte(addr); + const lastByte = this.readByte(addr + 1); + const firstLetter = firstByte & 0x7F; + const lastLetter = lastByte & 0x7F; + + const name = + String.fromCharCode(firstLetter) + + (lastLetter ? String.fromCharCode(lastLetter) : ''); + const type = (lastByte & 0x80) >> 7 | (firstByte & 0x80) >> 6; + + return { name, type }; + } + + private readArray(addr: word, type: byte, sizes: number[]): ApplesoftArray { + let strLen, strAddr; + let value; + const ary = []; + const len = sizes[0]; + + for (let idx = 0; idx < len; idx++) { + if (sizes.length > 1) { + value = this.readArray(addr, type, sizes.slice(1)); + } else { + switch (type) { + case 0: // Real + value = this.readFloat(addr); + addr += 5; + break; + case 1: // String + strLen = this.readByte(addr); + strAddr = this.readWord(addr + 1); + value = this.readString(strLen, strAddr); + addr += 3; + break; + case 3: // Integer + default: + value = this.readInt(addr); + addr += 2; + break; + } + } + ary[idx] = value; + } + return ary; + } + + dumpInternals() { + return { + txttab: this.readWord(TXTTAB), + fac: this.readFloat(FAC, { unpacked: true }), + arg: this.readFloat(ARG, { unpacked: true }), + curline: this.readWord(CURLINE), + }; + } + + dumpVariables() { + const simpleVariableTable = this.readWord(VARTAB); + const arrayVariableTable = this.readWord(ARYTAB); + const variableStorageEnd = this.readWord(STREND); + // var stringStorageStart = readWord(0x6F); + + let addr; + const vars: ApplesoftVariable[] = []; + let value; + let strLen, strAddr; + + for (addr = simpleVariableTable; addr < arrayVariableTable; addr += 7) { + const { name, type } = this.readVar(addr); + + switch (type) { + case VariableType.Float: + value = this.readFloat(addr + 2); + break; + case VariableType.String: + strLen = this.readByte(addr + 2); + strAddr = this.readWord(addr + 3); + value = this.readString(strLen, strAddr); + break; + case VariableType.Function: + value = toHex(this.readWord(addr + 2)); + value += ',' + toHex(this.readWord(addr + 4)); + break; + case VariableType.Integer: + value = this.readInt(addr + 2); + break; + } + vars.push({ name, type, value }); + } + + while (addr < variableStorageEnd) { + const { name, type } = this.readVar(addr); + const off = this.readWord(addr + 2); + const dim = this.readByte(addr + 4); + const sizes = []; + for (let idx = 0; idx < dim; idx++) { + sizes[idx] = this.readInt(addr + 5 + idx * 2); + } + value = this.readArray(addr + 5 + dim * 2, type, sizes); + vars.push({ name, sizes, type, value }); + + if (off < 1) { + break; + } + addr += off; + } + + return vars; + } +} diff --git a/js/applesoft/zeropage.ts b/js/applesoft/zeropage.ts index e347bb4..7b4b7dc 100644 --- a/js/applesoft/zeropage.ts +++ b/js/applesoft/zeropage.ts @@ -14,6 +14,12 @@ export const VARTAB = 0x69; export const ARYTAB = 0x6B; /** End of strings (word). (Strings are allocated down from HIMEM.) */ export const STREND = 0x6D; +/** Current line */ +export const CURLINE = 0x75; +/** Floating Point accumulator (float) */ +export const FAC = 0x9D; +/** Floating Point arguments (float) */ +export const ARG = 0xA5; /** * End of program (word). This is actually 1 or 2 bytes past the three * zero bytes that end the program. diff --git a/js/canvas.ts b/js/canvas.ts index 2769aa8..8fae70f 100644 --- a/js/canvas.ts +++ b/js/canvas.ts @@ -593,7 +593,7 @@ export class HiresPage2D implements HiresPage { const data = this.imageData.data; let dx, dy; - if ((rowa < 24) && (col < 40) && this.vm.hiresMode) { + if ((rowa < 24) && (col < 40) && (this.vm.hiresMode || this._refreshing)) { let y = rowa << 4 | rowb << 1; if (y < this.dirty.top) { this.dirty.top = y; } y += 1; @@ -916,6 +916,14 @@ export class VideoModes2D implements VideoModes { this._hgrs[page - 1] = hires; } + getLoresPage(page: pageNo) { + return this._grs[page - 1]; + } + + getHiresPage(page: pageNo) { + return this._hgrs[page - 1]; + } + text(on: boolean) { const old = this.textMode; this.textMode = on; diff --git a/js/cards/langcard.ts b/js/cards/langcard.ts index 0d65f9e..b9ac944 100644 --- a/js/cards/langcard.ts +++ b/js/cards/langcard.ts @@ -18,10 +18,10 @@ export default class LanguageCard implements Card, Restorable private bank2: RAM; private ram: RAM; - private readbsr = false; - private writebsr = false; - private bsr2 = false; - private prewrite = false; + private _readbsr = false; + private _writebsr = false; + private _bsr2 = false; + private _prewrite = false; private read1: Memory; private read2: Memory; @@ -48,16 +48,16 @@ export default class LanguageCard implements Card, Restorable } private updateBanks() { - if (this.readbsr) { - this.read1 = this.bsr2 ? this.bank2 : this.bank1; + if (this._readbsr) { + this.read1 = this._bsr2 ? this.bank2 : this.bank1; this.read2 = this.ram; } else { this.read1 = this.rom; this.read2 = this.rom; } - if (this.writebsr) { - this.write1 = this.bsr2 ? this.bank2 : this.bank1; + if (this._writebsr) { + this.write1 = this._bsr2 ? this.bank2 : this.bank1; this.write2 = this.ram; } else { this.write1 = this.rom; @@ -90,35 +90,35 @@ export default class LanguageCard implements Card, Restorable if (writeSwitch) { // $C081, $C083, $C089, $C08B if (readMode) { - this.writebsr = this.prewrite; + this._writebsr = this._prewrite; } - this.prewrite = readMode; + this._prewrite = readMode; if (offSwitch) { // $C083, $C08B - this.readbsr = true; + this._readbsr = true; rwStr = 'Read/Write'; } else { // $C081, $C089 - this.readbsr = false; + this._readbsr = false; rwStr = 'Write'; } } else { // $C080, $C082, $C088, $C08A - this.writebsr = false; - this.prewrite = false; + this._writebsr = false; + this._prewrite = false; if (offSwitch) { // $C082, $C08A - this.readbsr = false; + this._readbsr = false; rwStr = 'Off'; } else { // $C080, $C088 - this.readbsr = true; + this._readbsr = true; rwStr = 'Read'; } } if (bank1Switch) { // C08[8-C] - this.bsr2 = false; + this._bsr2 = false; bankStr = 'Bank 1'; } else { // C08[0-3] - this.bsr2 = true; + this._bsr2 = true; bankStr = 'Bank 2'; } @@ -158,12 +158,24 @@ export default class LanguageCard implements Card, Restorable } } + public get bsr2() { + return this._bsr2; + } + + public get readbsr() { + return this._readbsr; + } + + public get writebsr() { + return this._writebsr; + } + getState() { return { readbsr: this.readbsr, writebsr: this.writebsr, bsr2: this.bsr2, - prewrite: this.prewrite, + prewrite: this._prewrite, ram: this.ram.getState(), bank1: this.bank1.getState(), bank2: this.bank2.getState() @@ -171,10 +183,10 @@ export default class LanguageCard implements Card, Restorable } setState(state: LanguageCardState) { - this.readbsr = state.readbsr; - this.writebsr = state.writebsr; - this.bsr2 = state.bsr2; - this.prewrite = state.prewrite; + this._readbsr = state.readbsr; + this._writebsr = state.writebsr; + this._bsr2 = state.bsr2; + this._prewrite = state.prewrite; this.ram.setState(state.ram); this.bank1.setState(state.bank1); this.bank2.setState(state.bank2); diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index a1b7d6f..2b9ce97 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -3,7 +3,7 @@ import cs from 'classnames'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { Apple2 as Apple2Impl } from '../apple2'; import { ControlStrip } from './ControlStrip'; -import { Debugger } from './Debugger'; +import { Debugger } from './debugger/Debugger'; import { ErrorModal } from './ErrorModal'; import { Inset } from './Inset'; import { Keyboard } from './Keyboard'; @@ -151,7 +151,7 @@ export const Apple2 = (props: Apple2Props) => { - {showDebug ? : null} + {showDebug ? : null} ); }; diff --git a/js/components/Debugger.tsx b/js/components/Debugger.tsx deleted file mode 100644 index 6c8790d..0000000 --- a/js/components/Debugger.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { h, JSX } from 'preact'; -import cs from 'classnames'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { Apple2 as Apple2Impl } from '../apple2'; -import { ControlButton } from './ControlButton'; -import { FileChooser } from './FileChooser'; -import { Inset } from './Inset'; -import { loadLocalBinaryFile } from './util/files'; - -import styles from './css/Debugger.module.css'; -import { spawn } from './util/promises'; -import { toHex } from 'js/util'; - -export interface DebuggerProps { - apple2: Apple2Impl | undefined; - e: boolean; -} - -interface DebugData { - memory: string; - registers: string; - running: boolean; - stack: string; - trace: string; - zeroPage: string; -} - -const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i; -const VALID_PAGE = /^[0-9A-F]{1,2}$/i; -const VALID_ADDRESS = /^[0-9A-F]{1,4}$/i; - -const ERROR_ICON = ( -
- -
-); - -export const Debugger = ({ apple2 }: DebuggerProps) => { - const debug = apple2?.getDebugger(); - const [data, setData] = useState(); - const [memoryPage, setMemoryPage] = useState('08'); - const [loadAddress, setLoadAddress] = useState('0800'); - const [run, setRun] = useState(true); - const animationRef = useRef(0); - - const animate = useCallback(() => { - if (debug) { - setData({ - registers: debug.dumpRegisters(), - running: debug.isRunning(), - stack: debug.getStack(38), - trace: debug.getTrace(16), - zeroPage: debug.dumpPage(0), - memory: debug.dumpPage(parseInt(memoryPage, 16) || 0) - }); - } - animationRef.current = requestAnimationFrame(animate); - }, [debug, memoryPage]); - - useEffect(() => { - animationRef.current = requestAnimationFrame(animate); - return () => cancelAnimationFrame(animationRef.current); - }, [animate]); - - const doPause = useCallback(() => { - apple2?.stop(); - }, [apple2]); - - const doRun = useCallback(() => { - apple2?.run(); - }, [apple2]); - - const doStep = useCallback(() => { - debug?.step(); - }, [debug]); - - const doLoadAddress = useCallback((event: JSX.TargetedEvent) => { - setLoadAddress(event.currentTarget.value); - }, []); - const doRunCheck = useCallback((event: JSX.TargetedEvent) => { - setRun(event.currentTarget.checked); - }, []); - - const doMemoryPage = useCallback((event: JSX.TargetedEvent) => { - setMemoryPage(event.currentTarget.value); - }, []); - - const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => { - if (debug && handles.length === 1) { - spawn(async () => { - const file = await handles[0].getFile(); - let atAddress = parseInt(loadAddress, 16) || 0x800; - - const matches = file.name.match(CIDERPRESS_EXTENSION); - if (matches && matches.length === 3) { - const [, , aux] = matches; - atAddress = parseInt(aux, 16); - } - - await loadLocalBinaryFile(file, atAddress, debug); - setLoadAddress(toHex(atAddress, 4)); - if (run) { - debug?.runAt(atAddress); - } - }); - } - }, [debug, loadAddress, run]); - - if (!data) { - return null; - } - - const { - memory, - registers, - running, - stack, - trace, - zeroPage - } = data; - - const memoryPageValid = VALID_PAGE.test(memoryPage); - const loadAddressValid = VALID_ADDRESS.test(loadAddress); - - return ( - -
-
Debugger
- Controls -
- {running ? ( - - ) : ( - - )} - -
-
-
- Registers -
-                            {registers}
-                        
- Trace -
-                            {trace}
-                        
- ZP -
-                            {zeroPage}
-                        
-
-
- Stack -
-                            {stack}
-                        
-
-
-
-
- Memory Page: $ - - {memoryPageValid ? null : ERROR_ICON} -
-                        {memory}
-                    
-
-
-
- Load File: $ - - {loadAddressValid ? null : ERROR_ICON} - {' '} - Run -
- -
-
-
-
- ); -}; diff --git a/js/components/Keyboard.tsx b/js/components/Keyboard.tsx index 87f29d3..4b38861 100644 --- a/js/components/Keyboard.tsx +++ b/js/components/Keyboard.tsx @@ -118,6 +118,9 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => { if (document.activeElement && document.activeElement !== document.body) { return; } + if (event.key === ' ') { + event.preventDefault(); + } const key = mapKeyEvent(event, active.includes('LOCK')); if (key !== 0xff) { // CTRL-SHIFT-DELETE for reset diff --git a/js/components/Tabs.tsx b/js/components/Tabs.tsx new file mode 100644 index 0000000..d66a109 --- /dev/null +++ b/js/components/Tabs.tsx @@ -0,0 +1,63 @@ +import { ComponentChild, ComponentChildren, h } from 'preact'; +import { useCallback, useState } from 'preact/hooks'; +import cs from 'classnames'; + +import styles from './css/Tabs.module.css'; + +export interface TabProps { + children: ComponentChildren; +} + +export const Tab = ({ children }: TabProps) => { + return ( +
+ {children} +
+ ); +}; + +interface TabWrapperProps { + children: ComponentChild; + onClick: () => void; + selected: boolean; +} + +const TabWrapper = ({ children, onClick, selected }: TabWrapperProps) => { + return ( +
+ {children} +
+ ); +}; + +export interface TabsProps { + children: ComponentChildren; + setSelected: (selected: number) => void; +} + +export const Tabs = ({ children, setSelected }: TabsProps) => { + const [innerSelected, setInnerSelected] = useState(0); + + const innerSetSelected = useCallback((idx: number) => { + setSelected(idx); + setInnerSelected(idx); + }, [setSelected]); + + if (!Array.isArray(children)) { + return null; + } + + return ( +
+ {children.map((child, idx) => + innerSetSelected(idx)} + selected={idx === innerSelected} + > + {child} + + )} +
+ ); +}; diff --git a/js/components/css/Tabs.module.css b/js/components/css/Tabs.module.css new file mode 100644 index 0000000..9a243c0 --- /dev/null +++ b/js/components/css/Tabs.module.css @@ -0,0 +1,23 @@ +.tab { + border-top: 2px groove; + border-left: 2px groove; + border-right: 2px groove; + margin: 0 2px; + font-weight: bold; + padding: 4px; + border-radius: 4px 4px 0 0; +} + +.tab.selected { + background-color: #c4c1a0; + border-bottom: none; + margin-bottom: -2px; + color: #080; +} + +.tabs { + display: flex; + flex-direction: row; + border-bottom: 2px groove; + margin-bottom: 6px; +} diff --git a/js/components/debugger/Applesoft.tsx b/js/components/debugger/Applesoft.tsx new file mode 100644 index 0000000..b953a8e --- /dev/null +++ b/js/components/debugger/Applesoft.tsx @@ -0,0 +1,137 @@ +import { h } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; + +import { toHex } from 'js/util'; +import ApplesoftDecompiler from 'js/applesoft/decompiler'; +import { ApplesoftHeap, ApplesoftVariable } from 'js/applesoft/heap'; +import { Apple2 as Apple2Impl } from 'js/apple2'; + +import styles from './css/Applesoft.module.css'; +import debuggerStyles from './css/Debugger.module.css'; + +export interface ApplesoftProps { + apple2: Apple2Impl | undefined; +} + +interface ApplesoftData { + variables: ApplesoftVariable[]; + internals: { + txttab?: number; + fac?: number; + arg?: number; + curline?: number; + }; + listing: string; +} + +const TYPE_SYMBOL = ['', '$', '()', '%'] as const; +const TYPE_NAME = ['Float', 'String', 'Function', 'Integer'] as const; + +const formatArray = (value: unknown): string => { + if (Array.isArray(value)) { + if (Array.isArray(value[0])) { + return `[${value.map((x) => formatArray(x)).join(',\n ')}]`; + } else { + return `[${value.map((x) => formatArray(x)).join(', ')}]`; + } + } else { + return `${JSON.stringify(value)}`; + } +}; + +const Variable = ({ variable }: { variable: ApplesoftVariable }) => { + const { name, type, sizes, value } = variable; + const isArray = !!sizes; + const arrayStr = isArray ? `(${sizes.map((size) => size - 1).join(',')})` : ''; + return ( + + {name}{TYPE_SYMBOL[type]}{arrayStr} + {TYPE_NAME[type]}{isArray ? ' Array' : ''} + {isArray ? formatArray(value) : value} + + ); +}; + +export const Applesoft = ({ apple2 }: ApplesoftProps) => { + const animationRef = useRef(0); + const [data, setData] = useState({ + listing: '', + variables: [], + internals: {} + }); + const [heap, setHeap] = useState(); + const cpu = apple2?.getCPU(); + + useEffect(() => { + if (cpu) { + // setDecompiler(); + setHeap(new ApplesoftHeap(cpu)); + } + }, [cpu]); + + const animate = useCallback(() => { + if (cpu && heap) { + try { + const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu); + setData({ + variables: heap.dumpVariables(), + internals: heap.dumpInternals(), + listing: decompiler.decompile() + }); + } catch (error) { + if (error instanceof Error) { + setData({ + variables: [], + internals: {}, + listing: error.message + }); + } else { + throw error; + } + } + } + animationRef.current = requestAnimationFrame(animate); + }, [cpu, heap]); + + useEffect(() => { + animationRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(animationRef.current); + }, [animate]); + + const { listing, internals, variables } = data; + + return ( +
+ Listing +
{listing}
+ Variables +
+ + + + + + + {variables.map((variable, idx) => )} +
NameTypeValue
+
+ Internals +
+ + + + + + + + + + + + + +
TXTTAB{toHex(internals.txttab ?? 0)}FAC{internals.fac}
ARG{internals.arg}CURLINE{internals.curline}
+
+
+ ); +}; diff --git a/js/components/debugger/CPU.tsx b/js/components/debugger/CPU.tsx new file mode 100644 index 0000000..eb78829 --- /dev/null +++ b/js/components/debugger/CPU.tsx @@ -0,0 +1,211 @@ +import { h, JSX } from 'preact'; +import cs from 'classnames'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { Apple2 as Apple2Impl } from '../../apple2'; +import { ControlButton } from '../ControlButton'; +import { FileChooser } from '../FileChooser'; +import { loadLocalBinaryFile } from '../util/files'; +import { spawn } from '../util/promises'; +import { toHex } from 'js/util'; + +import styles from './css/CPU.module.css'; +import debuggerStyles from './css/Debugger.module.css'; + +export interface CPUProps { + apple2: Apple2Impl | undefined; +} + +interface DebugData { + memory: string; + registers: string; + running: boolean; + stack: string; + trace: string; + zeroPage: string; +} + +const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i; +const VALID_PAGE = /^[0-9A-F]{1,2}$/i; +const VALID_ADDRESS = /^[0-9A-F]{1,4}$/i; + +const ERROR_ICON = ( +
+ +
+); + +export const CPU = ({ apple2 }: CPUProps) => { + const debug = apple2?.getDebugger(); + const [data, setData] = useState({ + running: true, + registers: '', + stack: '', + trace: '', + zeroPage: '', + memory: '', + }); + const [memoryPage, setMemoryPage] = useState('08'); + const [loadAddress, setLoadAddress] = useState('0800'); + const [run, setRun] = useState(true); + const animationRef = useRef(0); + + const animate = useCallback(() => { + if (debug) { + setData({ + registers: debug.dumpRegisters(), + running: debug.isRunning(), + stack: debug.getStack(38), + trace: debug.getTrace(16), + zeroPage: debug.dumpPage(0), + memory: debug.dumpPage(parseInt(memoryPage, 16) || 0) + }); + } + animationRef.current = requestAnimationFrame(animate); + }, [debug, memoryPage]); + + useEffect(() => { + animationRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(animationRef.current); + }, [animate]); + + const doPause = useCallback(() => { + apple2?.stop(); + }, [apple2]); + + const doRun = useCallback(() => { + apple2?.run(); + }, [apple2]); + + const doStep = useCallback(() => { + debug?.step(); + }, [debug]); + + const doLoadAddress = useCallback((event: JSX.TargetedEvent) => { + setLoadAddress(event.currentTarget.value); + }, []); + const doRunCheck = useCallback((event: JSX.TargetedEvent) => { + setRun(event.currentTarget.checked); + }, []); + + const doMemoryPage = useCallback((event: JSX.TargetedEvent) => { + setMemoryPage(event.currentTarget.value); + }, []); + + const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => { + if (debug && handles.length === 1) { + spawn(async () => { + const file = await handles[0].getFile(); + let atAddress = parseInt(loadAddress, 16) || 0x800; + + const matches = file.name.match(CIDERPRESS_EXTENSION); + if (matches && matches.length === 3) { + const [, , aux] = matches; + atAddress = parseInt(aux, 16); + } + + await loadLocalBinaryFile(file, atAddress, debug); + setLoadAddress(toHex(atAddress, 4)); + if (run) { + debug?.runAt(atAddress); + } + }); + } + }, [debug, loadAddress, run]); + + const { + memory, + registers, + running, + stack, + trace, + zeroPage + } = data; + + const memoryPageValid = VALID_PAGE.test(memoryPage); + const loadAddressValid = VALID_ADDRESS.test(loadAddress); + + return ( +
+ Controls +
+ {running ? ( + + ) : ( + + )} + +
+
+
+ Registers +
+                        {registers}
+                    
+ Trace +
+                        {trace}
+                    
+ ZP +
+                        {zeroPage}
+                    
+
+
+ Stack +
+                        {stack}
+                    
+
+
+
+
+ Memory Page: $ + + {memoryPageValid ? null : ERROR_ICON} +
+                    {memory}
+                
+
+
+
+ Load File: $ + + {loadAddressValid ? null : ERROR_ICON} + {' '} + Run +
+ +
+
+
+ ); +}; diff --git a/js/components/debugger/Debugger.tsx b/js/components/debugger/Debugger.tsx new file mode 100644 index 0000000..a928689 --- /dev/null +++ b/js/components/debugger/Debugger.tsx @@ -0,0 +1,40 @@ +import { h } from 'preact'; +import { Inset } from '../Inset'; +import { Tab, Tabs } from '../Tabs'; +import { Apple2 } from 'js/apple2'; +import { useState } from 'preact/hooks'; +import { CPU } from './CPU'; + +import styles from './css/Debugger.module.css'; +import { Applesoft } from './Applesoft'; +import { Memory } from './Memory'; +import { VideoModes } from './VideoModes'; + +interface DebuggerProps { + apple2: Apple2 | undefined; +} + +export const Debugger = ({ apple2 }: DebuggerProps) => { + const [selected, setSelected] = useState(0); + + if (!apple2) { + return null; + } + + return ( + + + CPU + Video + Memory + Applesoft + +
+ {selected === 0 ? : null} + {selected === 1 ? : null} + {selected === 2 ? : null} + {selected === 3 ? : null} +
+
+ ); +}; diff --git a/js/components/debugger/Memory.tsx b/js/components/debugger/Memory.tsx new file mode 100644 index 0000000..6fa6e81 --- /dev/null +++ b/js/components/debugger/Memory.tsx @@ -0,0 +1,363 @@ +import { ComponentChildren, h } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import cs from 'classnames'; + +import { Apple2 as Apple2Impl } from 'js/apple2'; +import MMU from 'js/mmu'; +import LanguageCard from 'js/cards/langcard'; + +import styles from './css/Memory.module.css'; +import debuggerStyles from './css/Debugger.module.css'; + +/** + * Encapsulates the read/write status of a bank + */ +interface ReadWrite { + read: boolean; + write: boolean; +} + +/** + * Encapsulates the read/write status of a language card + */ + interface LC extends ReadWrite { + bank0: ReadWrite; + bank1: ReadWrite; + rom: ReadWrite; +} + +/** + * Encapsulates the read/write status of an aux/main memory bank. + */ +interface Bank extends ReadWrite { + lc: LC; + hires: ReadWrite; + text: ReadWrite; + zp: ReadWrite; +} + +/** + * Encapsulates the read/write status of aux main memory and rom banks. + */ +interface Banks { + main: Bank; + aux: Bank; + io: ReadWrite; + intcxrom: ReadWrite; +} + +/** + * Computes a language card state for an MMU aux or main bank. + * + * @param mmu MMU object + * @param altzp Compute for main or aux bank + * @returns LC read/write state + */ +const calcLC = (mmu: MMU, altzp: boolean) => { + const read = mmu.readbsr && (mmu.altzp === altzp); + const write = mmu.writebsr && (mmu.altzp === altzp); + return { + read, + write, + bank0: { + read: read && !mmu.bank1, + write: write && !mmu.bank1, + }, + bank1: { + read: read && mmu.bank1, + write: write && mmu.bank1, + }, + rom: { + read: !mmu.readbsr, + write: !mmu.writebsr, + }, + }; +}; + +/** + * Computes the hires aux or main read/write status. + * + * @param mmu MMU object + * @param aux Compute for main or aux bank + * @returns Hires pags read/write state + */ +const calcHires = (mmu: MMU, aux: boolean) => { + const page2sel = mmu.hires && mmu._80store; + return { + read: page2sel ? mmu.page2 === aux : mmu.auxread === aux, + write: page2sel ? mmu.page2 === aux : mmu.auxwrite === aux, + }; +}; + +/** + * Computes the text aux or main read/write status. + * + * @param mmu MMU object + * @param aux Compute for main or aux bank + * @returns Text page read/write state + */ +const calcText = (mmu: MMU, aux: boolean) => { + const page2sel = mmu._80store; + return { + read: page2sel ? mmu.page2 === aux : mmu.auxread === aux, + write: page2sel ? mmu.page2 === aux : mmu.auxwrite === aux, + }; +}; + +/** + * Creates read/write state from a flag + * + * @param flag Read/write flag + * @returns A read/write state + */ +const readAndWrite = (flag: boolean) => { + return { + read: flag, + write: flag, + }; +}; + +/** + * Computes the aux or main bank read/write status. + * + * @param mmu MMU object + * @param aux Compute for main or aux bank + * @returns read/write state + */ +const calcBanks = (mmu: MMU): Banks => { + return { + main: { + read: !mmu.auxread, + write: !mmu.auxwrite, + lc: calcLC(mmu, false), + hires: calcHires(mmu, false), + text: calcText(mmu, false), + zp: readAndWrite(!mmu.altzp), + }, + aux: { + read: mmu.auxread, + write: mmu.auxwrite, + lc: calcLC(mmu, true), + hires: calcHires(mmu, true), + text: calcText(mmu, true), + zp: readAndWrite(mmu.altzp), + }, + io: readAndWrite(!mmu.intcxrom), + intcxrom: readAndWrite(mmu.intcxrom), + }; +}; + +/** + * Computes the read/write state of a language card. + * + * @param card The language card + * @returns read/write state + */ +const calcLanguageCard = (card: LanguageCard): LC => { + const read = card.readbsr; + const write = card.writebsr; + return { + read, + write, + bank0: { + read: read && !card.bsr2, + write: write && !card.bsr2, + }, + bank1: { + read: read && card.bsr2, + write: write && card.bsr2, + }, + rom: { + read: !card.readbsr, + write: !card.writebsr, + } + }; +}; + +/** + * Computes the classes for a bank from read/write state. + * + * @param rw Read/write state + * @returns Classes + */ +const rw = (rw: ReadWrite) => { + return { + [styles.read]: rw.read, + [styles.write]: rw.write, + [styles.inactive]: !rw.write && !rw.read, + }; +}; + +/** + * Properties for LanguageCard component + */ +interface LanguageCardMapProps { + lc: LC; + children?: ComponentChildren; +} + +/** + * Language card state component use by both the MMU and LanguageCard + * visualizations. + * + * @param lc LC state + * @param children label component + * @returns LanguageCard component + */ +const LanguageCardMap = ({lc, children}: LanguageCardMapProps) => { + return ( +
+
+ {children} LC +
+
+
+ Bank 0 +
+
+ Bank 1 +
+
+
+ ); +}; + +/** + * Legend of state colors. Green for read, red for write, blue for both, grey for + * inactive. + * + * @returns Legend component + */ +const Legend = () => { + return ( +
+
+
Read +
+
+
Write +
+
+
Read/Write +
+
+
Inactive +
+
+ ); +}; + +/** + * Properties for the Memory component. + */ +export interface MemoryProps { + apple2: Apple2Impl | undefined; +} + +/** + * Memory debugger component. Displays the active state of banks of + * memory - aux, 80 column and language card depending up the machine. + * + * @param apple2 Apple2 object + * @returns Memory component + */ +export const Memory = ({ apple2 }: MemoryProps) => { + const animationRef = useRef(0); + const [banks, setBanks] = useState(); + const [lc, setLC] = useState(); + + const animate = useCallback(() => { + if (apple2) { + const mmu = apple2.getMMU(); + if (mmu) { + setBanks(calcBanks(mmu)); + } else { + const card = apple2.getIO().getSlot(0); + if (card instanceof LanguageCard) { + setLC(calcLanguageCard(card)); + } + } + } + animationRef.current = requestAnimationFrame(animate); + }, [apple2]); + + useEffect(() => { + animationRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(animationRef.current); + }, [animate]); + + if (banks) { + return ( +
+
MMU
+
+ + Aux + + + Main + +
+
+ ROM +
+
+
+
+
+ IO +
+
+ CXROM +
+
+
+
+ Aux Mem +
+ Hires +
+
+ Text/Lores +
+
+ Stack/ZP +
+
+
+ Main Mem +
+ Hires +
+
+ Text/Lores +
+
+ Stack/ZP +
+
+
+
+ +
+ ); + } else if (lc) { + return ( +
+
Language Card
+
+ +
+
+ ROM +
+
+
+
+ +
+ ); + } else { + return null; + } +}; diff --git a/js/components/debugger/VideoModes.tsx b/js/components/debugger/VideoModes.tsx new file mode 100644 index 0000000..ec0afaa --- /dev/null +++ b/js/components/debugger/VideoModes.tsx @@ -0,0 +1,94 @@ +import { h } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import cs from 'classnames'; + +import { Apple2 as Apple2Impl } from 'js/apple2'; +import { VideoPage } from 'js/videomodes'; + +import styles from './css/VideoModes.module.css'; +import debuggerStyles from './css/Debugger.module.css'; + +export interface VideoModesProps { + apple2: Apple2Impl | undefined; +} + +const blit = (page: VideoPage, canvas: HTMLCanvasElement | null) => { + if (canvas) { + const context = canvas.getContext('2d'); + if (context) { + context.putImageData(page.imageData, 0, 0); + } + } +}; + +export const VideoModes = ({ apple2 }: VideoModesProps) => { + const [text, setText] = useState(false); + const [hires, setHires] = useState(false); + const [page2, setPage2] = useState(false); + const canvas1 = useRef(null); + const canvas2 = useRef(null); + const canvas3 = useRef(null); + const canvas4 = useRef(null); + const animationRef = useRef(0); + + const animate = useCallback(() => { + if (apple2) { + const vm = apple2.getVideoModes(); + const text = vm.isText(); + const hires = vm.isHires(); + const page2 = vm.isPage2(); + + vm.getLoresPage(1).refresh(); + vm.getLoresPage(2).refresh(); + vm.getHiresPage(1).refresh(); + vm.getHiresPage(2).refresh(); + blit(vm.getLoresPage(1), canvas1.current); + blit(vm.getLoresPage(2), canvas2.current); + blit(vm.getHiresPage(1), canvas3.current); + blit(vm.getHiresPage(2), canvas4.current); + + setText(text); + setHires(hires); + setPage2(page2); + } + animationRef.current = requestAnimationFrame(animate); + }, [apple2]); + + useEffect(() => { + animationRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(animationRef.current); + }, [animate]); + + return ( +
+
+
+
+ Text/Lores Page 1 +
+ +
+
+
+ Text/Lores Page 2 +
+ +
+
+
+
+
+ Hires Page 1 +
+ +
+
+
+ Hires Page 2 +
+ +
+
+
+ ); +}; diff --git a/js/components/debugger/css/Applesoft.module.css b/js/components/debugger/css/Applesoft.module.css new file mode 100644 index 0000000..78c44b3 --- /dev/null +++ b/js/components/debugger/css/Applesoft.module.css @@ -0,0 +1,48 @@ +.listing { + width: calc(100% - 12px); + height: 320px; + overflow: auto; + white-space: pre-wrap; +} + +.variables { + width: 100%; + height: 320px; + overflow: auto; +} + +.variables table { + width: 100%; +} + +.variables td { + background-color: #fff; + border: 1px inset; + white-space: pre; + font-family: monospace; +} + +.internals { + width: 100%; +} + +.internals table { + width: 100%; +} + +.internals td { + background-color: #fff; + border: 1px inset; + white-space: pre; + font-family: monospace; + width: 30%; +} + +.internals th { + width: 20%; + text-align: right; +} + +.stack { + width: 10em; +} diff --git a/js/components/debugger/css/CPU.module.css b/js/components/debugger/css/CPU.module.css new file mode 100644 index 0000000..b953324 --- /dev/null +++ b/js/components/debugger/css/CPU.module.css @@ -0,0 +1,35 @@ +.controls { + padding: 3px 0; + margin: 2px 0; +} + +.zeroPage { + width: 53em; +} + +.trace { + width: 53em; +} + +.stack { + width: 10em; +} + +.fileChooser { + padding: 5px 0; +} + +.invalid { + color: #f00; +} + +div.invalid { + position: relative; + display: inline-block; +} + +div.invalid i { + position: absolute; + top: -9px; + left: -16px; +} diff --git a/js/components/css/Debugger.module.css b/js/components/debugger/css/Debugger.module.css similarity index 61% rename from js/components/css/Debugger.module.css rename to js/components/debugger/css/Debugger.module.css index 1f66a56..f9ef8d0 100644 --- a/js/components/css/Debugger.module.css +++ b/js/components/debugger/css/Debugger.module.css @@ -1,43 +1,14 @@ -.debugger pre { - font-size: 9px; - background: white; - color: black; - padding: 3px; - margin: 2px; - border: 1px inset; -} - -.debugger { - font-size: 12px; -} - -.debugger button, -.debugger input { - font-size: 12px; -} - -.debugger input[type="text"] { - border: 1px inset; -} - -.debugger hr { - color: #c4c1a0; -} - .inset { margin: 5px 10px 0; + display: flex; + flex-direction: column; width: auto; } .heading { font-weight: bold; font-size: 18px; - margin-bottom: 10px; -} - -.controls { - padding: 3px 0; - margin: 2px 0; + margin: 5px 0; } .subHeading { @@ -55,33 +26,29 @@ flex-direction: column; } -.zeroPage { - width: 50em; +.debugger { + font-size: 12px; + width: 590px; } -.trace { - width: 50em; +.debugger pre { + font-size: 9px; + background: white; + color: black; + padding: 3px; + margin: 2px; + border: 1px inset; } -.stack { - width: 10em; +.debugger button, +.debugger input { + font-size: 12px; } -.fileChooser { - padding: 5px 0; +.debugger input[type="text"] { + border: 1px inset; } -.invalid { - color: #f00; -} - -div.invalid { - position: relative; - display: inline-block; -} - -div.invalid i { - position: absolute; - top: -9px; - left: -16px; +.debugger hr { + color: #c4c1a0; } diff --git a/js/components/debugger/css/Memory.module.css b/js/components/debugger/css/Memory.module.css new file mode 100644 index 0000000..7995ee6 --- /dev/null +++ b/js/components/debugger/css/Memory.module.css @@ -0,0 +1,154 @@ +.memory { + width: auto; + box-sizing: border-box; +} + +.bank { + width: 128px; + border-right: 1px solid #000; +} + +.bank, +.bank div, +.io, +.rom, +.intcxrom { + text-align: center; + justify-content: center; +} + +.upperMemory { + height: 96px; + width: 385px; + border-bottom: 1px solid #000; + border-left: 1px solid #000; + border-top: 1px solid #000; +} + +.upperMemory .bank { + height: 96px; +} + +.rom { + display: flex; + align-items: center; + height: 96px; + width: 128px; +} + +.intcxrom { + display: flex; + align-items: center; + height: 32px; + width: 128px; + border-right: 1px solid #000; + border-bottom: 1px solid #000; +} + +.languageCard { + width: 256px; + height: 96px; + border: 1px solid #000; +} + +.lc { + display: flex; + align-items: center; + position: relative; + width: 127px; + height: 63px; +} + +.lcbanks { + width: 127px; + display: flex; + flex-direction: row; +} + +.lcbank { + display: flex; + align-items: center; + width: 64px; + height: 31px; + border-top: 1px solid #000; +} + +.lcbank0 { + border-right: 1px solid #000; +} + +.io { + display: flex; + align-items: center; + width: 255px; + height: 32px; + border-left: 1px solid #000; + border-right: 1px solid #000; + border-bottom: 1px solid #000; +} + +.hires { + display: flex; + align-items: center; + height: 64px; + width: 127px; + position: absolute; + bottom: 64px; + border-top: 1px solid #000; + border-bottom: 1px solid #000; +} + +.text { + display: flex; + align-items: center; + height: 16px; + width: 127px; + position: absolute; + bottom: 24px; + border-top: 1px solid #000; + border-bottom: 1px solid #000; +} + +.zp { + display: flex; + align-items: center; + height: 16px; + width: 127px; + position: absolute; + bottom: 0; + border-top: 1px solid #000; +} + +.lowerMemory { + border-left: 1px solid #000; + border-bottom: 1px solid #000; + width: 256px; +} + +.lowerMemory .bank { + height: 256px; + position: relative; +} + +div.read { + background-color: #8f8; +} + +div.write { + background-color: #f88; +} + +div.read.write { + background-color: #88f; +} + +div.inactive { + background-color: #bbb; +} + +.legend { + width: 1em; + height: 1em; + display: inline-block; + border: 1px solid #000; +} diff --git a/js/components/debugger/css/VideoModes.module.css b/js/components/debugger/css/VideoModes.module.css new file mode 100644 index 0000000..3f0a483 --- /dev/null +++ b/js/components/debugger/css/VideoModes.module.css @@ -0,0 +1,10 @@ +.pages canvas { + border: 1px inset; + width: 280px; + height: 192px; + margin: 5px; +} + +.active canvas { + border: 1px inset #00f; +} diff --git a/js/cpu6502.ts b/js/cpu6502.ts index 0421492..ada5640 100644 --- a/js/cpu6502.ts +++ b/js/cpu6502.ts @@ -1535,10 +1535,10 @@ export default class CPU6502 { public read(a: number, b?: number): byte { let page, off; if (b !== undefined) { - page = a; - off = b; + page = a & 0xff; + off = b & 0xff; } else { - page = a >> 8; + page = (a >> 8) & 0xff; off = a & 0xff; } return this.memPages[page].read(page, off); @@ -1551,13 +1551,13 @@ export default class CPU6502 { let page, off, val; if (c !== undefined ) { - page = a; - off = b; - val = c; + page = a & 0xff; + off = b & 0xff; + val = c & 0xff; } else { - page = a >> 8; + page = (a >> 8) & 0xff; off = a & 0xff; - val = b; + val = b & 0xff; } this.memPages[page].write(page, off, val); } diff --git a/js/debugger.ts b/js/debugger.ts index e8a0926..78f3fcf 100644 --- a/js/debugger.ts +++ b/js/debugger.ts @@ -6,7 +6,6 @@ import CPU6502, { DebugInfo, flags, sizes } from './cpu6502'; export interface DebuggerContainer { run: () => void; stop: () => void; - getCPU: () => CPU6502; isRunning: () => boolean; } @@ -28,16 +27,13 @@ export const dumpStatusRegister = (sr: byte) => ].join(''); 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(); - } + constructor(private cpu: CPU6502, private container: DebuggerContainer) {} stepCycles(cycles: number) { this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => { diff --git a/js/gl.ts b/js/gl.ts index f654e1b..09d0edc 100644 --- a/js/gl.ts +++ b/js/gl.ts @@ -662,6 +662,14 @@ export class VideoModesGL implements VideoModes { this._hgrs[page - 1] = hires; } + getLoresPage(page: pageNo) { + return this._grs[page - 1]; + } + + getHiresPage(page: pageNo) { + return this._hgrs[page - 1]; + } + text(on: boolean) { const old = this.textMode; this.textMode = on; diff --git a/js/mmu.ts b/js/mmu.ts index 75cc3ee..edc0791 100644 --- a/js/mmu.ts +++ b/js/mmu.ts @@ -169,7 +169,7 @@ export default class MMU implements Memory, Restorable { private _altzp: boolean; // Video - private _80store: boolean; + private __80store: boolean; private _page2: boolean; private _hires: boolean; @@ -289,7 +289,7 @@ export default class MMU implements Memory, Restorable { } } - _initSwitches() { + private _initSwitches() { this._bank1 = false; this._readbsr = false; this._writebsr = false; @@ -303,14 +303,14 @@ export default class MMU implements Memory, Restorable { this._slot3rom = false; this._intc8rom = false; - this._80store = false; + this.__80store = false; this._page2 = false; this._hires = false; this._iouDisable = true; } - _debug(..._args: unknown[]) { + private _debug(..._args: unknown[]) { // debug.apply(this, _args); } @@ -339,7 +339,7 @@ export default class MMU implements Memory, Restorable { } } - if (this._80store) { + if (this.__80store) { if (this._page2) { for (let idx = 0x4; idx < 0x8; idx++) { this._readPages[idx] = this._pages[idx][1]; @@ -440,15 +440,15 @@ export default class MMU implements Memory, Restorable { // Apple //e memory management - _accessMMUSet(off: byte, _val?: byte) { + private _accessMMUSet(off: byte, _val?: byte) { switch (off) { case LOC._80STOREOFF: - this._80store = false; + this.__80store = false; this._debug('80 Store Off', _val); this.vm.page(this._page2 ? 2 : 1); break; case LOC._80STOREON: - this._80store = true; + this.__80store = true; this._debug('80 Store On', _val); break; case LOC.RAMRDOFF: @@ -520,7 +520,7 @@ export default class MMU implements Memory, Restorable { // Status registers - _accessStatus(off: byte, val?: byte) { + private _accessStatus(off: byte, val?: byte) { let result = undefined; switch(off) { @@ -553,8 +553,8 @@ export default class MMU implements Memory, Restorable { result = this._slot3rom ? 0x80 : 0x00; break; case LOC._80STORE: // 0xC018 - this._debug(`80 Store ${this._80store ? 'true' : 'false'}`); - result = this._80store ? 0x80 : 0x00; + this._debug(`80 Store ${this.__80store ? 'true' : 'false'}`); + result = this.__80store ? 0x80 : 0x00; break; case LOC.VERTBLANK: // 0xC019 // result = cpu.getCycles() % 20 < 5 ? 0x80 : 0x00; @@ -585,7 +585,7 @@ export default class MMU implements Memory, Restorable { return result; } - _accessIOUDisable(off: byte, val?: byte) { + private _accessIOUDisable(off: byte, val?: byte) { const writeMode = val !== undefined; let result; @@ -614,13 +614,13 @@ export default class MMU implements Memory, Restorable { } - _accessGraphics(off: byte, val?: byte) { + private _accessGraphics(off: byte, val?: byte) { let result: byte | undefined = 0; switch (off) { case LOC.PAGE1: this._page2 = false; - if (!this._80store) { + if (!this.__80store) { result = this.io.ioSwitch(off, val); } this._debug('Page 2 off'); @@ -628,7 +628,7 @@ export default class MMU implements Memory, Restorable { case LOC.PAGE2: this._page2 = true; - if (!this._80store) { + if (!this.__80store) { result = this.io.ioSwitch(off, val); } this._debug('Page 2 on'); @@ -671,7 +671,7 @@ export default class MMU implements Memory, Restorable { return result; } - _accessLangCard(off: byte, val?: byte) { + private _accessLangCard(off: byte, val?: byte) { const readMode = val === undefined; const result = readMode ? 0 : undefined; @@ -801,6 +801,46 @@ export default class MMU implements Memory, Restorable { this._vbEnd = this.cpu.getCycles() + 1000; } + public get bank1() { + return this._bank1; + } + + public get readbsr() { + return this._readbsr; + } + + public get writebsr() { + return this._writebsr; + } + + public get auxread() { + return this._auxRamRead; + } + + public get auxwrite() { + return this._auxRamWrite; + } + + public get altzp() { + return this._altzp; + } + + public get _80store() { + return this.__80store; + } + + public get page2() { + return this._page2; + } + + public get hires() { + return this._hires; + } + + public get intcxrom() { + return this._intcxrom; + } + public getState(): MMUState { return { bank1: this._bank1, @@ -816,7 +856,7 @@ export default class MMU implements Memory, Restorable { auxRamWrite: this._auxRamWrite, altzp: this._altzp, - _80store: this._80store, + _80store: this.__80store, page2: this._page2, hires: this._hires, @@ -853,7 +893,7 @@ export default class MMU implements Memory, Restorable { this._auxRamWrite = state.auxRamWrite; this._altzp = state.altzp; - this._80store = state._80store; + this.__80store = state._80store; this._page2 = state.page2; this._hires = state.hires; diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 431bb4f..d575233 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -22,6 +22,7 @@ import Tape, { TAPE_TYPES } from './tape'; import ApplesoftDecompiler from '../applesoft/decompiler'; import ApplesoftCompiler from '../applesoft/compiler'; +import { TXTTAB } from 'js/applesoft/zeropage'; import { debug } from '../util'; import { Apple2, Stats, State as Apple2State } from '../apple2'; @@ -90,18 +91,15 @@ let ready: Promise<[void, void]>; export const driveLights = new DriveLights(); -/** Start of program (word) */ -const TXTTAB = 0x67; - -export function dumpAppleSoftProgram() { +export function dumpApplesoftProgram() { const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu); debug(decompiler.list({apple2: _e ? 'e' : 'plus'})); } -export function compileAppleSoftProgram(program: string) { +export function compileApplesoftProgram(program: string) { const start = cpu.read(TXTTAB) + (cpu.read(TXTTAB + 1) << 8); ApplesoftCompiler.compileToMemory(cpu, program, start); - dumpAppleSoftProgram(); + dumpApplesoftProgram(); } export function openLoad(driveString: string, event: MouseEvent) { diff --git a/js/videomodes.ts b/js/videomodes.ts index 057a19b..7a5a7a9 100644 --- a/js/videomodes.ts +++ b/js/videomodes.ts @@ -67,7 +67,9 @@ export interface VideoModes extends Restorable { reset(): void; setLoresPage(page: pageNo, lores: LoresPage): void; + getLoresPage(page: pageNo): LoresPage; setHiresPage(page: pageNo, lores: HiresPage): void; + getHiresPage(page: pageNo): HiresPage; _80col(on: boolean): void; altChar(on: boolean): void; diff --git a/test/js/debugger.spec.ts b/test/js/debugger.spec.ts index b7241e7..6ee9fae 100644 --- a/test/js/debugger.spec.ts +++ b/test/js/debugger.spec.ts @@ -18,12 +18,11 @@ describe('Debugger', () => { cpu.addPageHandler(bios); debuggerContainer = { - getCPU: () => cpu, run: jest.fn(), stop: jest.fn(), isRunning: jest.fn(), }; - theDebugger = new Debugger(debuggerContainer); + theDebugger = new Debugger(cpu, debuggerContainer); }); describe('#utility', () => {