diff --git a/js/apple2.ts b/js/apple2.ts index 283992d..fc1d26b 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -217,6 +217,10 @@ export class Apple2 implements Restorable, DebuggerContainer { this.runAnimationFrame = null; } + isRunning() { + return !this.paused; + } + getState(): State { const state: State = { cpu: this.cpu.getState(), diff --git a/js/components/App.tsx b/js/components/App.tsx index ff35d54..8e9e1fe 100644 --- a/js/components/App.tsx +++ b/js/components/App.tsx @@ -1,5 +1,5 @@ import 'preact/debug'; -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import { Header } from './Header'; import { Apple2 } from './Apple2'; import { usePrefs } from './hooks/usePrefs'; @@ -7,7 +7,7 @@ import { SYSTEM_TYPE_APPLE2E } from '../ui/system'; import { SCREEN_GL } from '../ui/screen'; import { defaultSystem, systemTypes } from './util/systems'; -import './css/App.module.css'; +import styles from './css/App.module.css'; /** * Top level application component, provides the parameters @@ -26,12 +26,12 @@ export const App = () => { }; return ( - <> +
- +
); }; diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index 4a3bca3..b955094 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -1,10 +1,11 @@ import { h } from 'preact'; import cs from 'classnames'; -import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { Apple2 as Apple2Impl } from '../apple2'; import Apple2IO from '../apple2io'; import CPU6502 from '../cpu6502'; import { ControlStrip } from './ControlStrip'; +import { Debugger } from './Debugger'; import { ErrorModal } from './ErrorModal'; import { Inset } from './Inset'; import { Keyboard } from './Keyboard'; @@ -47,6 +48,7 @@ export const Apple2 = (props: Apple2Props) => { const [cpu, setCPU] = useState(); const [error, setError] = useState(); const [ready, setReady] = useState(false); + const [showDebug, setShowDebug] = useState(false); const drivesReady = useMemo(() => new Ready(setError), []); useEffect(() => { @@ -84,21 +86,37 @@ export const Apple2 = (props: Apple2Props) => { } }, [props, drivesReady]); + const toggleDebugger = useCallback(() => { + setShowDebug((on) => !on); + }, []); + + const removeFocus = useCallback(() => { + if (document?.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }, []); + return ( -
- - - {!e ? : null} - - - - - - - - - - +
+
+ + + {!e ? : null} + + + + + + + + + + +
+ {showDebug ? : null}
); }; diff --git a/js/components/ControlStrip.tsx b/js/components/ControlStrip.tsx index eeea766..d0fe6b9 100644 --- a/js/components/ControlStrip.tsx +++ b/js/components/ControlStrip.tsx @@ -6,7 +6,6 @@ import { useHotKey } from './hooks/useHotKey'; import { AudioControl } from './AudioControl'; import { OptionsModal} from './OptionsModal'; import { OptionsContext } from './OptionsContext'; -import { PauseControl } from './PauseControl'; import { Printer } from './Printer'; import { ControlButton } from './ControlButton'; import { Apple2 as Apple2Impl } from '../apple2'; @@ -22,6 +21,7 @@ const README = 'https://github.com/whscullin/apple2js#readme'; interface ControlStripProps { apple2: Apple2Impl | undefined; e: boolean; + toggleDebugger: () => void; } /** @@ -33,7 +33,7 @@ interface ControlStripProps { * @param e Whether or not this is a //e * @returns ControlStrip component */ -export const ControlStrip = ({ apple2, e }: ControlStripProps) => { +export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) => { const [showOptions, setShowOptions] = useState(false); const [io, setIO] = useState(); const options = useContext(OptionsContext); @@ -87,7 +87,7 @@ export const ControlStrip = ({ apple2, e }: ControlStripProps) => { - +
diff --git a/js/components/Debugger.tsx b/js/components/Debugger.tsx new file mode 100644 index 0000000..2fbaca4 --- /dev/null +++ b/js/components/Debugger.tsx @@ -0,0 +1,143 @@ +import { h, JSX } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { Apple2 as Apple2Impl } from '../apple2'; +import { Inset } from './Inset'; + +import styles from './css/Debugger.module.css'; +import { ControlButton } from './ControlButton'; + +export interface DebuggerProps { + apple2: Apple2Impl | undefined; + e: boolean; +} + +interface DebugData { + memory: string; + registers: string; + running: boolean; + stack: string; + trace: string; + zeroPage: string; +} + +export const Debugger = ({ apple2 }: DebuggerProps) => { + const debug = apple2?.getDebugger(); + const [data, setData] = useState(); + const [memoryPage, setMemoryPage] = useState('08'); + 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 doMemoryPage = useCallback((event: JSX.TargetedMouseEvent) => { + setMemoryPage(event.currentTarget.value); + }, []); + + if (!data) { + return null; + } + + const { + memory, + registers, + running, + stack, + trace, + zeroPage + } = data; + + return ( + +
+
Debugger
+ Controls +
+ {running ? ( + + ) : ( + + )} + +
+
+
+ Registers +
+                            {registers}
+                        
+ Trace +
+                            {trace}
+                        
+ ZP +
+                            {zeroPage}
+                        
+
+
+ Stack +
+                            {stack}
+                        
+
+
+
+ Memory Page: $ + +
+                        {memory}
+                    
+
+
+
+ ); +}; diff --git a/js/components/Inset.tsx b/js/components/Inset.tsx index 94f0366..31d8ee4 100644 --- a/js/components/Inset.tsx +++ b/js/components/Inset.tsx @@ -1,14 +1,19 @@ -import { h, ComponentChildren } from 'preact'; +import { h, ComponentChildren, JSX } from 'preact'; +import cs from 'classnames'; import styles from './css/Inset.module.css'; +interface InsetProps extends JSX.HTMLAttributes { + children: ComponentChildren; +} + /** * Convenience component for a nice beveled border. * * @returns Inset component */ -export const Inset = ({ children }: { children: ComponentChildren }) => ( -
+export const Inset = ({ children, className, ...props }: InsetProps) => ( +
{children}
); diff --git a/js/components/Keyboard.tsx b/js/components/Keyboard.tsx index eacee4e..87f29d3 100644 --- a/js/components/Keyboard.tsx +++ b/js/components/Keyboard.tsx @@ -115,6 +115,9 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => { // Set global keystroke handler useEffect(() => { const keyDown = (event: KeyboardEvent) => { + if (document.activeElement && document.activeElement !== document.body) { + return; + } const key = mapKeyEvent(event, active.includes('LOCK')); if (key !== 0xff) { // CTRL-SHIFT-DELETE for reset diff --git a/js/components/PauseControl.tsx b/js/components/PauseControl.tsx deleted file mode 100644 index f183613..0000000 --- a/js/components/PauseControl.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { h, Fragment } from 'preact'; -import { useCallback, useState } from 'preact/hooks'; -import { Apple2 as Apple2Impl } from 'js/apple2'; -import { ControlButton } from './ControlButton'; - -/** - * PauseControl component properties. - */ -export interface PauseControlProps { - apple2: Apple2Impl | undefined; -} - -/** - * Provides a control to pause and unpause the CPU. - * - * @param apple2 The Apple2 object - * @returns PauseControl component - */ -export const PauseControl = ({ apple2 }: PauseControlProps) => { - const [running, setRunning] = useState(true); - - const doPause = useCallback(() => { - apple2?.stop(); - setRunning(false); - }, [apple2]); - - const doRun = useCallback(() => { - apple2?.run(); - setRunning(true); - }, [apple2]); - - return ( - <> - {running ? ( - - ) : ( - - )} - - ); -}; diff --git a/js/components/css/App.module.css b/js/components/css/App.module.css index 8e96d27..2dae220 100644 --- a/js/components/css/App.module.css +++ b/js/components/css/App.module.css @@ -36,3 +36,8 @@ button:focus, a[role="button"]:focus { a[role="button"] { text-decoration: none; } + +.container { + display: flex; + flex-direction: column; +} diff --git a/js/components/css/Apple2.module.css b/js/components/css/Apple2.module.css index b67b81a..9440541 100644 --- a/js/components/css/Apple2.module.css +++ b/js/components/css/Apple2.module.css @@ -1,5 +1,8 @@ -.outer { +.container { + display: flex; margin: auto; +} +.outer { width: 620px; display: none; } diff --git a/js/components/css/Debugger.module.css b/js/components/css/Debugger.module.css new file mode 100644 index 0000000..7cc849e --- /dev/null +++ b/js/components/css/Debugger.module.css @@ -0,0 +1,56 @@ +.debugger pre { + font-size: 9px; + background: white; + color: black; + padding: 3px; + margin: 2px; + border: 1px inset; +} + +.debugger input { + border: 1px inset; + font-size: 12px; +} + +.inset { + margin: 5px 10px; + width: auto; +} + +.heading { + font-weight: bold; + font-size: 16px; + margin-bottom: 10px; +} + +.controls { + padding: 3px 0; + margin: 2px 0; +} + +.subHeading { + font-weight: bold; + font-size: 12px; +} + +.row { + display: flex; + flex-direction: row; +} + +.column { + display: flex; + flex-direction: column; +} + +.zeroPage { + width: 50em; +} + +.trace { + width: 50em; +} + +.stack { + width: 10em; +} diff --git a/js/debugger.ts b/js/debugger.ts index d2021f7..eff60ad 100644 --- a/js/debugger.ts +++ b/js/debugger.ts @@ -7,6 +7,7 @@ export interface DebuggerContainer { run: () => void; stop: () => void; getCPU: () => CPU6502; + isRunning: () => boolean; } type symbols = { [key: number]: string }; @@ -71,6 +72,9 @@ export default class Debugger { this.container.run(); }; + isRunning = () => + this.container.isRunning(); + setVerbose = (verbose: boolean) => { this.verbose = verbose; }; @@ -79,12 +83,39 @@ export default class Debugger { this.maxTrace = maxTrace; }; - getTrace = () => { - return this.trace.map(this.printDebugInfo).join('\n'); + getTrace = (count?: number) => { + return this.trace.slice(count ? -count : undefined).map(this.printDebugInfo).join('\n'); }; - printTrace = () => { - debug(this.getTrace()); + printTrace = (count?: number) => { + debug(this.getTrace(count)); + }; + + getStack = (size?: number) => { + const { sp } = this.cpu.getDebugInfo(); + const stack = []; + + let max = 255; + let min = 0; + if (size) { + if ((sp - 3) >= (255 - size)) { + min = Math.max(255 - size + 1, 0); + } else { + max = Math.min(sp + size - 4, 255); + min = Math.max(sp - 3, 0); + } + } + + for (let addr = max; addr >= min; addr--) { + const isSP = addr === sp ? '*' : ' '; + const addrStr = `$${toHex(0x0100 + addr)}`; + const valStr = toHex(this.cpu.read(0x01, addr)); + if (!size || ((sp + size > addr) && (addr > sp - size))) { + stack.push(`${isSP} ${addrStr} ${valStr}`); + } + } + + return stack.join('\n'); }; setBreakpoint = (addr: word, exp?: breakpointFn) => { diff --git a/test/js/debugger.spec.ts b/test/js/debugger.spec.ts index 6e7b01b..b7241e7 100644 --- a/test/js/debugger.spec.ts +++ b/test/js/debugger.spec.ts @@ -21,6 +21,7 @@ describe('Debugger', () => { getCPU: () => cpu, run: jest.fn(), stop: jest.fn(), + isRunning: jest.fn(), }; theDebugger = new Debugger(debuggerContainer); }); @@ -55,5 +56,55 @@ describe('Debugger', () => { 'A=00 X=00 Y=00 P=20 S=FF --X-----' ); }); + + it('should dump the stack,', () => { + const stack = theDebugger.getStack(); + const lines = stack.split('\n'); + expect(lines).toHaveLength(256); + expect(lines[0]).toMatch('* $01FF 00'); + expect(lines[1]).toMatch(' $01FE 00'); + expect(lines[254]).toMatch(' $0101 00'); + expect(lines[255]).toMatch(' $0100 00'); + }); + + it('should dump the stack with size', () => { + const stack = theDebugger.getStack(32); + const lines = stack.split('\n'); + expect(lines).toHaveLength(32); + expect(lines[0]).toMatch('* $01FF 00'); + expect(lines[1]).toMatch(' $01FE 00'); + expect(lines[30]).toMatch(' $01E1 00'); + expect(lines[31]).toMatch(' $01E0 00'); + }); + + it('should dump the stack within size', () => { + const registers = cpu.getState(); + registers.sp = 0xE3; + cpu.setState(registers); + const stack = theDebugger.getStack(32); + const lines = stack.split('\n'); + expect(lines).toHaveLength(32); + expect(lines[0]).toMatch(' $01FF 00'); + expect(lines[1]).toMatch(' $01FE 00'); + expect(lines[28]).toMatch('* $01E3 00'); + expect(lines[29]).toMatch(' $01E2 00'); + expect(lines[30]).toMatch(' $01E1 00'); + expect(lines[31]).toMatch(' $01E0 00'); + }); + + it('should dump the stack with size and move the window', () => { + const registers = cpu.getState(); + registers.sp = 0xC3; + cpu.setState(registers); + const stack = theDebugger.getStack(32); + const lines = stack.split('\n'); + expect(lines).toHaveLength(32); + expect(lines[0]).toMatch(' $01DF 00'); + expect(lines[1]).toMatch(' $01DE 00'); + expect(lines[28]).toMatch('* $01C3 00'); + expect(lines[29]).toMatch(' $01C2 00'); + expect(lines[30]).toMatch(' $01C1 00'); + expect(lines[31]).toMatch(' $01C0 00'); + }); }); });