More debugger panels (#141)

This commit is contained in:
Will Scullin 2022-07-13 20:34:50 -07:00 committed by GitHub
parent fd5217158e
commit c0ff1e8129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1595 additions and 355 deletions

View File

@ -152,7 +152,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
return; // already running
}
this.theDebugger = new Debugger(this);
this.theDebugger = new Debugger(this.cpu, this);
this.theDebugger.addSymbols(SYMBOLS);
const interval = 30;

View File

@ -412,6 +412,10 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
this._slot[slot] = card;
}
getSlot(slot: slot): Card | null {
return this._slot[slot];
}
keyDown(ascii: byte) {
this._keyDown = true;
this._key = ascii | 0x80;

View File

@ -124,13 +124,13 @@ export default class ApplesoftCompiler {
private lines: Map<number, byte[]> = 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

View File

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

200
js/applesoft/heap.ts Normal file
View File

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

View File

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

View File

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

View File

@ -18,10 +18,10 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
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<LanguageCardState>
}
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<LanguageCardState>
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<LanguageCardState>
}
}
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<LanguageCardState>
}
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);

View File

@ -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) => {
</Inset>
<ErrorModal error={error} setError={setError} />
</div>
{showDebug ? <Debugger apple2={apple2} e={e} /> : null}
{showDebug ? <Debugger apple2={apple2} /> : null}
</div>
);
};

View File

@ -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 = (
<div className={styles.invalid}>
<i
className="fa-solid fa-triangle-exclamation"
title="Invalid hex address"
/>
</div>
);
export const Debugger = ({ apple2 }: DebuggerProps) => {
const debug = apple2?.getDebugger();
const [data, setData] = useState<DebugData>();
const [memoryPage, setMemoryPage] = useState('08');
const [loadAddress, setLoadAddress] = useState('0800');
const [run, setRun] = useState(true);
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 doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
}, []);
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
}, []);
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
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 (
<Inset className={styles.inset}>
<div className={cs(styles.debugger, styles.column)}>
<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>
<hr />
<span className={styles.subHeading}>Memory Page: $ </span>
<input
value={memoryPage}
onChange={doMemoryPage}
maxLength={2}
className={cs({ [styles.invalid]: !memoryPageValid })}
/>
{memoryPageValid ? null : ERROR_ICON}
<pre className={styles.zp}>
{memory}
</pre>
</div>
<div>
<hr />
<span className={styles.subHeading}>Load File: $ </span>
<input
type="text"
value={loadAddress}
maxLength={4}
onChange={doLoadAddress}
className={cs({ [styles.invalid]: !loadAddressValid })}
/>
{loadAddressValid ? null : ERROR_ICON}
{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
<div className={styles.fileChooser}>
<FileChooser onChange={doChooseFile} />
</div>
</div>
</div>
</Inset>
);
};

View File

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

63
js/components/Tabs.tsx Normal file
View File

@ -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 (
<div>
{children}
</div>
);
};
interface TabWrapperProps {
children: ComponentChild;
onClick: () => void;
selected: boolean;
}
const TabWrapper = ({ children, onClick, selected }: TabWrapperProps) => {
return (
<div onClick={onClick} className={cs(styles.tab, { [styles.selected]: selected })}>
{children}
</div>
);
};
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 (
<div className={styles.tabs}>
{children.map((child, idx) =>
<TabWrapper
key={idx}
onClick={() => innerSetSelected(idx)}
selected={idx === innerSelected}
>
{child}
</TabWrapper>
)}
</div>
);
};

View File

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

View File

@ -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 (
<tr>
<td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td>
<td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td>
<td>{isArray ? formatArray(value) : value}</td>
</tr>
);
};
export const Applesoft = ({ apple2 }: ApplesoftProps) => {
const animationRef = useRef<number>(0);
const [data, setData] = useState<ApplesoftData>({
listing: '',
variables: [],
internals: {}
});
const [heap, setHeap] = useState<ApplesoftHeap>();
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 (
<div className={styles.column}>
<span className={debuggerStyles.subHeading}>Listing</span>
<pre className={styles.listing}>{listing}</pre>
<span className={debuggerStyles.subHeading}>Variables</span>
<div className={styles.variables}>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Value</th>
</tr>
{variables.map((variable, idx) => <Variable key={idx} variable={variable} />)}
</table>
</div>
<span className={debuggerStyles.subHeading}>Internals</span>
<div className={styles.internals}>
<table>
<tr>
<th>TXTTAB</th>
<td>{toHex(internals.txttab ?? 0)}</td>
<th>FAC</th>
<td>{internals.fac}</td>
</tr>
<tr>
<th>ARG</th>
<td>{internals.arg}</td>
<th>CURLINE</th>
<td>{internals.curline}</td>
</tr>
</table>
</div>
</div>
);
};

View File

@ -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 = (
<div className={styles.invalid}>
<i
className="fa-solid fa-triangle-exclamation"
title="Invalid hex address"
/>
</div>
);
export const CPU = ({ apple2 }: CPUProps) => {
const debug = apple2?.getDebugger();
const [data, setData] = useState<DebugData>({
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<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 doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
}, []);
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
}, []);
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
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 (
<div className={debuggerStyles.column}>
<span className={debuggerStyles.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={debuggerStyles.row}>
<div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Registers</span>
<pre>
{registers}
</pre>
<span className={debuggerStyles.subHeading}>Trace</span>
<pre className={styles.trace}>
{trace}
</pre>
<span className={debuggerStyles.subHeading}>ZP</span>
<pre className={styles.zeroPage}>
{zeroPage}
</pre>
</div>
<div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Stack</span>
<pre className={styles.stack}>
{stack}
</pre>
</div>
</div>
<div>
<hr />
<span className={debuggerStyles.subHeading}>Memory Page: $ </span>
<input
value={memoryPage}
onChange={doMemoryPage}
maxLength={2}
className={cs({ [styles.invalid]: !memoryPageValid })}
/>
{memoryPageValid ? null : ERROR_ICON}
<pre className={styles.zp}>
{memory}
</pre>
</div>
<div>
<hr />
<span className={debuggerStyles.subHeading}>Load File: $ </span>
<input
type="text"
value={loadAddress}
maxLength={4}
onChange={doLoadAddress}
className={cs({ [styles.invalid]: !loadAddressValid })}
/>
{loadAddressValid ? null : ERROR_ICON}
{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
<div className={styles.fileChooser}>
<FileChooser onChange={doChooseFile} />
</div>
</div>
</div>
);
};

View File

@ -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 (
<Inset className={styles.inset}>
<Tabs setSelected={setSelected}>
<Tab>CPU</Tab>
<Tab>Video</Tab>
<Tab>Memory</Tab>
<Tab>Applesoft</Tab>
</Tabs>
<div className={styles.debugger}>
{selected === 0 ? <CPU apple2={apple2} /> : null}
{selected === 1 ? <VideoModes apple2={apple2} /> : null}
{selected === 2 ? <Memory apple2={apple2} /> : null}
{selected === 3 ? <Applesoft apple2={apple2} /> : null}
</div>
</Inset>
);
};

View File

@ -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 (
<div className={cs(styles.bank)}>
<div className={cs(styles.lc, rw(lc))}>
{children} LC
</div>
<div className={styles.lcbanks}>
<div className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}>
Bank 0
</div>
<div className={cs(styles.lcbank, rw(lc.bank1))}>
Bank 1
</div>
</div>
</div>
);
};
/**
* Legend of state colors. Green for read, red for write, blue for both, grey for
* inactive.
*
* @returns Legend component
*/
const Legend = () => {
return (
<div>
<div>
<div className={cs(styles.read, styles.legend)}> </div> Read
</div>
<div>
<div className={cs(styles.write, styles.legend)}> </div> Write
</div>
<div>
<div className={cs(styles.write, styles.read, styles.legend)}> </div> Read/Write
</div>
<div>
<div className={cs(styles.inactive, styles.legend)}> </div> Inactive
</div>
</div>
);
};
/**
* 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<number>(0);
const [banks, setBanks] = useState<Banks>();
const [lc, setLC] = useState<LC>();
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 (
<div className={styles.memory}>
<div className={debuggerStyles.heading}>MMU</div>
<div className={cs(styles.upperMemory, debuggerStyles.row)}>
<LanguageCardMap lc={banks.aux.lc}>
Aux
</LanguageCardMap>
<LanguageCardMap lc={banks.main.lc}>
Main
</LanguageCardMap>
<div className={cs(styles.bank)}>
<div className={cs(styles.rom, rw(banks.main.lc.rom))}>
ROM
</div>
</div>
</div>
<div className={cs(debuggerStyles.row)}>
<div className={cs(styles.io, rw(banks.io))}>
IO
</div>
<div className={cs(styles.intcxrom, rw(banks.intcxrom))}>
CXROM
</div>
</div>
<div className={cs(styles.lowerMemory, debuggerStyles.row)}>
<div className={cs(styles.bank, rw(banks.aux))}>
Aux Mem
<div className={cs(styles.hires, rw(banks.aux.hires))}>
Hires
</div>
<div className={cs(styles.text, rw(banks.aux.text))}>
Text/Lores
</div>
<div className={cs(styles.zp, rw(banks.aux.zp))}>
Stack/ZP
</div>
</div>
<div className={cs(styles.bank, rw(banks.main))}>
Main Mem
<div className={cs(styles.hires, rw(banks.main.hires))}>
Hires
</div>
<div className={cs(styles.text, rw(banks.main.text))}>
Text/Lores
</div>
<div className={cs(styles.zp, rw(banks.main.zp))}>
<span>Stack/ZP</span>
</div>
</div>
</div>
<hr />
<Legend />
</div>
);
} else if (lc) {
return (
<div className={styles.memory}>
<div className={debuggerStyles.heading}>Language Card</div>
<div className={cs(debuggerStyles.row, styles.languageCard)}>
<LanguageCardMap lc={lc} />
<div className={cs(styles.bank)}>
<div className={cs(styles.rom, rw(lc.rom))}>
ROM
</div>
</div>
</div>
<hr />
<Legend />
</div>
);
} else {
return null;
}
};

View File

@ -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<HTMLCanvasElement>(null);
const canvas2 = useRef<HTMLCanvasElement>(null);
const canvas3 = useRef<HTMLCanvasElement>(null);
const canvas4 = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>(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 (
<div className={styles.pages}>
<div className={debuggerStyles.row}>
<div className={cs(styles.page, {[styles.active]: (text || !hires) && !page2})}>
<div className={debuggerStyles.heading}>
Text/Lores Page 1
</div>
<canvas width="560" height="192" ref={canvas1} />
</div>
<div className={cs(styles.page, {[styles.active]: (text || !hires) && page2})}>
<div className={debuggerStyles.heading}>
Text/Lores Page 2
</div>
<canvas width="560" height="192" ref={canvas2} />
</div>
</div>
<div className={debuggerStyles.row}>
<div className={cs(styles.page, {[styles.active]: (!text && hires) && !page2})}>
<div className={debuggerStyles.heading}>
Hires Page 1
</div>
<canvas width="560" height="192" ref={canvas3} />
</div>
<div className={cs(styles.page, {[styles.active]: (!text && hires) && page2})}>
<div className={debuggerStyles.heading}>
Hires Page 2
</div>
<canvas width="560" height="192" ref={canvas4} />
</div>
</div>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
.pages canvas {
border: 1px inset;
width: 280px;
height: 192px;
margin: 5px;
}
.active canvas {
border: 1px inset #00f;
}

View File

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

View File

@ -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<word, breakpointFn> = 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, () => {

View File

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

View File

@ -169,7 +169,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
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<MMUState> {
}
}
_initSwitches() {
private _initSwitches() {
this._bank1 = false;
this._readbsr = false;
this._writebsr = false;
@ -303,14 +303,14 @@ export default class MMU implements Memory, Restorable<MMUState> {
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<MMUState> {
}
}
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<MMUState> {
// 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<MMUState> {
// 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<MMUState> {
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<MMUState> {
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<MMUState> {
}
_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<MMUState> {
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<MMUState> {
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<MMUState> {
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<MMUState> {
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<MMUState> {
this._auxRamWrite = state.auxRamWrite;
this._altzp = state.altzp;
this._80store = state._80store;
this.__80store = state._80store;
this._page2 = state.page2;
this._hires = state.hires;

View File

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

View File

@ -67,7 +67,9 @@ export interface VideoModes extends Restorable<VideoModesState> {
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;

View File

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