mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
More debugger panels (#141)
This commit is contained in:
parent
fd5217158e
commit
c0ff1e8129
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
200
js/applesoft/heap.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
10
js/canvas.ts
10
js/canvas.ts
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
63
js/components/Tabs.tsx
Normal 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>
|
||||
);
|
||||
};
|
23
js/components/css/Tabs.module.css
Normal file
23
js/components/css/Tabs.module.css
Normal 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;
|
||||
}
|
137
js/components/debugger/Applesoft.tsx
Normal file
137
js/components/debugger/Applesoft.tsx
Normal 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>
|
||||
);
|
||||
};
|
211
js/components/debugger/CPU.tsx
Normal file
211
js/components/debugger/CPU.tsx
Normal 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>
|
||||
);
|
||||
};
|
40
js/components/debugger/Debugger.tsx
Normal file
40
js/components/debugger/Debugger.tsx
Normal 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>
|
||||
);
|
||||
};
|
363
js/components/debugger/Memory.tsx
Normal file
363
js/components/debugger/Memory.tsx
Normal 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;
|
||||
}
|
||||
};
|
94
js/components/debugger/VideoModes.tsx
Normal file
94
js/components/debugger/VideoModes.tsx
Normal 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>
|
||||
);
|
||||
};
|
48
js/components/debugger/css/Applesoft.module.css
Normal file
48
js/components/debugger/css/Applesoft.module.css
Normal 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;
|
||||
}
|
35
js/components/debugger/css/CPU.module.css
Normal file
35
js/components/debugger/css/CPU.module.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
154
js/components/debugger/css/Memory.module.css
Normal file
154
js/components/debugger/css/Memory.module.css
Normal 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;
|
||||
}
|
10
js/components/debugger/css/VideoModes.module.css
Normal file
10
js/components/debugger/css/VideoModes.module.css
Normal file
@ -0,0 +1,10 @@
|
||||
.pages canvas {
|
||||
border: 1px inset;
|
||||
width: 280px;
|
||||
height: 192px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.active canvas {
|
||||
border: 1px inset #00f;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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, () => {
|
||||
|
8
js/gl.ts
8
js/gl.ts
@ -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;
|
||||
|
76
js/mmu.ts
76
js/mmu.ts
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user