diff --git a/js/components/Debugger.tsx b/js/components/Debugger.tsx index 2fbaca4..6c8790d 100644 --- a/js/components/Debugger.tsx +++ b/js/components/Debugger.tsx @@ -1,10 +1,15 @@ 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 { ControlButton } from './ControlButton'; +import { spawn } from './util/promises'; +import { toHex } from 'js/util'; export interface DebuggerProps { apple2: Apple2Impl | undefined; @@ -20,10 +25,25 @@ interface DebugData { 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(() => { @@ -57,10 +77,38 @@ export const Debugger = ({ apple2 }: DebuggerProps) => { debug?.step(); }, [debug]); - const doMemoryPage = useCallback((event: JSX.TargetedMouseEvent) => { + 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; } @@ -74,9 +122,12 @@ export const Debugger = ({ apple2 }: DebuggerProps) => { zeroPage } = data; + const memoryPageValid = VALID_PAGE.test(memoryPage); + const loadAddressValid = VALID_ADDRESS.test(loadAddress); + return ( -
+
Debugger
Controls
@@ -125,18 +176,36 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
- Memory Page: $ +
+ Memory Page: $ + {memoryPageValid ? null : ERROR_ICON}
                         {memory}
                     
+
+
+ Load File: $ + + {loadAddressValid ? null : ERROR_ICON} + {' '} + Run +
+ +
+
); diff --git a/js/components/css/App.module.css b/js/components/css/App.module.css index 957b13a..ac7c77d 100644 --- a/js/components/css/App.module.css +++ b/js/components/css/App.module.css @@ -49,3 +49,29 @@ input[type="file"]::file-selector-button { display: flex; flex-direction: column; } + +input[type="checkbox"] { + appearance: none; + background-color: #65594d; + border: 1px inset #65594d; + padding: 7px; + top: 7px; + border-radius: 3px; + display: inline-block; + position: relative; +} + +input[type="checkbox"]:checked { + background-color: #65594d; + border: 1px inset #65594d; + color: #0d0; +} + +input[type="checkbox"]:checked::after { + content: "\2716"; + font-size: 12px; + position: absolute; + top: 0; + left: 2px; + color: #0d0; +} diff --git a/js/components/css/Debugger.module.css b/js/components/css/Debugger.module.css index 7cc849e..1f66a56 100644 --- a/js/components/css/Debugger.module.css +++ b/js/components/css/Debugger.module.css @@ -7,19 +7,31 @@ border: 1px inset; } -.debugger input { - 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; + margin: 5px 10px 0; width: auto; } .heading { font-weight: bold; - font-size: 16px; + font-size: 18px; margin-bottom: 10px; } @@ -54,3 +66,22 @@ .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/util/files.ts b/js/components/util/files.ts index 3a3dbd3..f02d7bd 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -1,4 +1,4 @@ -import { includes } from 'js/types'; +import { includes, word } from 'js/types'; import { initGamepad } from 'js/ui/gamepad'; import { BlockFormat, @@ -12,6 +12,7 @@ import { } from 'js/formats/types'; import Disk2 from 'js/cards/disk2'; import SmartPort from 'js/cards/smartport'; +import Debugger from 'js/debugger'; type ProgressCallback = (current: number, total: number) => void; @@ -255,3 +256,24 @@ export class SmartStorageBroker implements MassStorage { return null; } } + +/** + * Load binary file into memory. + * + * @param file File object to read into memory + * @param address Address at which to start load + * @param debug Debugger object + * @returns resolves to true if successful + */ +export const loadLocalBinaryFile = (file: File, address: word, debug: Debugger) => { + return new Promise((resolve, _reject) => { + const fileReader = new FileReader(); + fileReader.onload = function () { + const result = this.result as ArrayBuffer; + const bytes = new Uint8Array(result); + debug.setMemory(address, bytes); + resolve(true); + }; + fileReader.readAsArrayBuffer(file); + }); +}; diff --git a/js/debugger.ts b/js/debugger.ts index eff60ad..e8a0926 100644 --- a/js/debugger.ts +++ b/js/debugger.ts @@ -72,6 +72,17 @@ export default class Debugger { this.container.run(); }; + /** + * Restart at a given memory address. + * + * @param address Address to start execution + */ + + runAt = (address: word) => { + this.cpu.reset(); + this.cpu.setPC(address); + }; + isRunning = () => this.container.isRunning(); @@ -212,6 +223,35 @@ export default class Debugger { return result; }; + /** + * Reads a range of memory. Will wrap at memory limit. + * + * @param address Starting address to read memory + * @param length Length of memory to read. + * @returns Byte array containing memory + */ + getMemory(address: word, length: word) { + const bytes = new Uint8Array(length); + for (let idx = 0; idx < length; idx++) { + address &= 0xffff; + bytes[idx] = this.cpu.read(address++); + } + return bytes; + } + + /** + * Writes a range of memory. Will wrap at memory limit. + * + * @param address Starting address to write memory + * @param bytes Data to write + */ + setMemory(address: word, bytes: Uint8Array) { + for (const byte of bytes) { + address &= 0xffff; + this.cpu.write(address++, byte); + } + } + list = (pc: word) => { const results = []; for (let idx = 0; idx < 20; idx++) {