Cheap and cheerful debugger (#135)

* Cheap and cheerful debugger

* Try to manage focus
This commit is contained in:
Will Scullin 2022-06-19 19:42:34 -07:00 committed by GitHub
parent c7a7bcd19b
commit 466a7eed78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 349 additions and 81 deletions

View File

@ -217,6 +217,10 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.runAnimationFrame = null;
}
isRunning() {
return !this.paused;
}
getState(): State {
const state: State = {
cpu: this.cpu.getState(),

View File

@ -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 (
<>
<div className={styles.container}>
<Header e={system.e} />
<Apple2
gl={gl}
{...system}
/>
</>
</div>
);
};

View File

@ -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<CPU6502>();
const [error, setError] = useState<unknown>();
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 (
<div className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}>
<Screen screen={screen} />
<Slinky io={io} slot={2} />
{!e ? <Videoterm io={io} slot={3} /> : null}
<Mouse cpu={cpu} screen={screen} io={io} slot={4} />
<ThunderClock io={io} slot={5} />
<Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
</Inset>
<ControlStrip apple2={apple2} e={e} />
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>
<ErrorModal error={error} setError={setError} />
<div className={styles.container}>
<div
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
onClick={removeFocus}
>
<Screen screen={screen} />
<Slinky io={io} slot={2} />
{!e ? <Videoterm io={io} slot={3} /> : null}
<Mouse cpu={cpu} screen={screen} io={io} slot={4} />
<ThunderClock io={io} slot={5} />
<Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
</Inset>
<ControlStrip apple2={apple2} e={e} toggleDebugger={toggleDebugger} />
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>
<ErrorModal error={error} setError={setError} />
</div>
{showDebug ? <Debugger apple2={apple2} e={e} /> : null}
</div>
);
};

View File

@ -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<Apple2IO>();
const options = useContext(OptionsContext);
@ -87,7 +87,7 @@ export const ControlStrip = ({ apple2, e }: ControlStripProps) => {
<OptionsModal isOpen={showOptions} onClose={doCloseOptions} />
<Inset>
<CPUMeter apple2={apple2} />
<PauseControl apple2={apple2} />
<ControlButton onClick={toggleDebugger} title="Toggle Debugger" icon="bug" />
<AudioControl apple2={apple2} />
<Printer io={io} slot={1} />
<div style={{flexGrow: 1}} />

143
js/components/Debugger.tsx Normal file
View File

@ -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<DebugData>();
const [memoryPage, setMemoryPage] = useState('08');
const animationRef = useRef<number>(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<HTMLInputElement>) => {
setMemoryPage(event.currentTarget.value);
}, []);
if (!data) {
return null;
}
const {
memory,
registers,
running,
stack,
trace,
zeroPage
} = data;
return (
<Inset className={styles.inset}>
<div className={styles.debugger}>
<div className={styles.heading}>Debugger</div>
<span className={styles.subHeading}>Controls</span>
<div className={styles.controls}>
{running ? (
<ControlButton
onClick={doPause}
disabled={!apple2}
title="Pause"
icon="pause"
/>
) : (
<ControlButton
onClick={doRun}
disabled={!apple2}
title="Run"
icon="play"
/>
)}
<ControlButton
onClick={doStep}
disabled={!apple2 || running}
title="Step"
icon="forward-step"
/>
</div>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.subHeading}>Registers</span>
<pre>
{registers}
</pre>
<span className={styles.subHeading}>Trace</span>
<pre className={styles.trace}>
{trace}
</pre>
<span className={styles.subHeading}>ZP</span>
<pre className={styles.zeroPage}>
{zeroPage}
</pre>
</div>
<div className={styles.column}>
<span className={styles.subHeading}>Stack</span>
<pre className={styles.stack}>
{stack}
</pre>
</div>
</div>
<div>
<span className={styles.subHeading}>Memory Page: $</span>
<input
min={0x00}
max={0xff}
value={memoryPage}
onChange={doMemoryPage}
maxLength={2}
/>
<pre className={styles.zp}>
{memory}
</pre>
</div>
</div>
</Inset>
);
};

View File

@ -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<HTMLDivElement> {
children: ComponentChildren;
}
/**
* Convenience component for a nice beveled border.
*
* @returns Inset component
*/
export const Inset = ({ children }: { children: ComponentChildren }) => (
<div className={styles.inset}>
export const Inset = ({ children, className, ...props }: InsetProps) => (
<div className={cs(className, styles.inset)} {...props}>
{children}
</div>
);

View File

@ -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

View File

@ -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 ? (
<ControlButton
onClick={doPause}
disabled={!apple2}
title="Pause"
icon="pause"
/>
) : (
<ControlButton
onClick={doRun}
disabled={!apple2}
title="Run"
icon="play"
/>
)}
</>
);
};

View File

@ -36,3 +36,8 @@ button:focus, a[role="button"]:focus {
a[role="button"] {
text-decoration: none;
}
.container {
display: flex;
flex-direction: column;
}

View File

@ -1,5 +1,8 @@
.outer {
.container {
display: flex;
margin: auto;
}
.outer {
width: 620px;
display: none;
}

View File

@ -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;
}

View File

@ -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) => {

View File

@ -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');
});
});
});