mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Cheap and cheerful debugger (#135)
* Cheap and cheerful debugger * Try to manage focus
This commit is contained in:
parent
c7a7bcd19b
commit
466a7eed78
@ -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(),
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
143
js/components/Debugger.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -36,3 +36,8 @@ button:focus, a[role="button"]:focus {
|
||||
a[role="button"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
.outer {
|
||||
.container {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
}
|
||||
.outer {
|
||||
width: 620px;
|
||||
display: none;
|
||||
}
|
||||
|
56
js/components/css/Debugger.module.css
Normal file
56
js/components/css/Debugger.module.css
Normal 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;
|
||||
}
|
@ -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) => {
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user