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
|
return; // already running
|
||||||
}
|
}
|
||||||
|
|
||||||
this.theDebugger = new Debugger(this);
|
this.theDebugger = new Debugger(this.cpu, this);
|
||||||
this.theDebugger.addSymbols(SYMBOLS);
|
this.theDebugger.addSymbols(SYMBOLS);
|
||||||
|
|
||||||
const interval = 30;
|
const interval = 30;
|
||||||
|
@ -412,6 +412,10 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
|||||||
this._slot[slot] = card;
|
this._slot[slot] = card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSlot(slot: slot): Card | null {
|
||||||
|
return this._slot[slot];
|
||||||
|
}
|
||||||
|
|
||||||
keyDown(ascii: byte) {
|
keyDown(ascii: byte) {
|
||||||
this._keyDown = true;
|
this._keyDown = true;
|
||||||
this._key = ascii | 0x80;
|
this._key = ascii | 0x80;
|
||||||
|
@ -124,13 +124,13 @@ export default class ApplesoftCompiler {
|
|||||||
private lines: Map<number, byte[]> = new Map();
|
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
|
* @param mem Memory, including zero page, into which the program is
|
||||||
* loaded.
|
* loaded.
|
||||||
* @param program A string with a BASIC program to compile (tokenize).
|
* @param program A string with a BASIC program to compile (tokenize).
|
||||||
* @param programStart Optional start address of the program. Defaults to
|
* @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) {
|
static compileToMemory(mem: Memory, program: string, programStart: word = PROGRAM_START) {
|
||||||
const compiler = new ApplesoftCompiler();
|
const compiler = new ApplesoftCompiler();
|
||||||
@ -179,7 +179,7 @@ export default class ApplesoftCompiler {
|
|||||||
for (const possibleToken in STRING_TO_TOKEN) {
|
for (const possibleToken in STRING_TO_TOKEN) {
|
||||||
if (lineBuffer.lookingAtToken(possibleToken)) {
|
if (lineBuffer.lookingAtToken(possibleToken)) {
|
||||||
// NOTE(flan): This special token-preference
|
// 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).
|
// code (D5BE-D5CA in the Apple //e ROM).
|
||||||
|
|
||||||
// Found a token
|
// Found a token
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { byte, word, ReadonlyUint8Array, Memory } from '../types';
|
import { byte, word, ReadonlyUint8Array, Memory } from '../types';
|
||||||
|
import { toHex } from 'js/util';
|
||||||
import { TOKEN_TO_STRING, STRING_TO_TOKEN } from './tokens';
|
import { TOKEN_TO_STRING, STRING_TO_TOKEN } from './tokens';
|
||||||
import { TXTTAB, PRGEND } from './zeropage';
|
import { TXTTAB, PRGEND } from './zeropage';
|
||||||
|
|
||||||
@ -8,6 +9,24 @@ const LETTERS =
|
|||||||
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
||||||
'`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 {
|
interface ListOptions {
|
||||||
apple2: 'e' | 'plus';
|
apple2: 'e' | 'plus';
|
||||||
columns: number; // usually 40 or 80
|
columns: number; // usually 40 or 80
|
||||||
@ -26,6 +45,8 @@ const DEFAULT_DECOMPILE_OPTIONS: DecompileOptions = {
|
|||||||
style: 'pretty',
|
style: 'pretty',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_LINES = 32768;
|
||||||
|
|
||||||
export default class ApplesoftDecompiler {
|
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 start = ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
|
||||||
const end = ram.read(0x00, PRGEND) + (ram.read(0x00, PRGEND + 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++) {
|
for (let addr = start; addr <= end; addr++) {
|
||||||
program.push(ram.read(addr >> 8, addr & 0xff));
|
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
|
* @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.
|
* is the offset of the line number of the line; the tokens follow.
|
||||||
*/
|
*/
|
||||||
private forEachLine(from: number, to: number,
|
private forEachLine(
|
||||||
callback: (offset: word) => void): void {
|
from: number, to: number,
|
||||||
|
callback: (offset: word) => void): void
|
||||||
|
{
|
||||||
|
let count = 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let nextLineAddr = this.wordAt(offset);
|
let nextLineAddr = this.wordAt(offset);
|
||||||
let nextLineNo = this.wordAt(offset + 2);
|
let nextLineNo = this.wordAt(offset + 2);
|
||||||
while (nextLineAddr !== 0 && nextLineNo < from) {
|
while (nextLineAddr !== 0 && nextLineNo < from) {
|
||||||
|
if (++count > MAX_LINES) {
|
||||||
|
throw new Error('Loop detected in listing');
|
||||||
|
}
|
||||||
offset = nextLineAddr;
|
offset = nextLineAddr;
|
||||||
nextLineAddr = this.wordAt(offset);
|
nextLineAddr = this.wordAt(offset);
|
||||||
nextLineNo = this.wordAt(offset + 2);
|
nextLineNo = this.wordAt(offset + 2);
|
||||||
}
|
}
|
||||||
while (nextLineAddr !== 0 && nextLineNo <= to) {
|
while (nextLineAddr !== 0 && nextLineNo <= to) {
|
||||||
|
if (++count > MAX_LINES) {
|
||||||
|
throw new Error('Loop detected in listing');
|
||||||
|
}
|
||||||
callback(offset + 2);
|
callback(offset + 2);
|
||||||
offset = nextLineAddr - this.base;
|
offset = nextLineAddr - this.base;
|
||||||
nextLineAddr = this.wordAt(offset);
|
nextLineAddr = this.wordAt(offset);
|
||||||
@ -113,13 +145,17 @@ export default class ApplesoftDecompiler {
|
|||||||
// always assumes that there is space for one token—which would
|
// always assumes that there is space for one token—which would
|
||||||
// have been the case on a realy Apple.
|
// have been the case on a realy Apple.
|
||||||
while (this.program[offset] !== 0) {
|
while (this.program[offset] !== 0) {
|
||||||
|
if (offset >= this.program.length) {
|
||||||
|
lines.unshift('Unterminated line: ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
const token = this.program[offset];
|
const token = this.program[offset];
|
||||||
if (token >= 0x80 && token <= 0xea) {
|
if (token >= 0x80 && token <= 0xea) {
|
||||||
line += ' '; // D750, always put a space in front of token
|
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
|
line += ' '; // D762, always put a trailing space
|
||||||
} else {
|
} else {
|
||||||
line += LETTERS[token];
|
line += resolveToken(token);
|
||||||
}
|
}
|
||||||
offset++;
|
offset++;
|
||||||
|
|
||||||
@ -194,17 +230,14 @@ export default class ApplesoftDecompiler {
|
|||||||
offset += 2;
|
offset += 2;
|
||||||
|
|
||||||
while (this.program[offset] !== 0) {
|
while (this.program[offset] !== 0) {
|
||||||
|
if (offset >= this.program.length) {
|
||||||
|
return 'Unterminated line: ' + result;
|
||||||
|
}
|
||||||
const token = this.program[offset];
|
const token = this.program[offset];
|
||||||
let tokenString: string;
|
let tokenString = resolveToken(token);
|
||||||
if (token >= 0x80 && token <= 0xea) {
|
|
||||||
tokenString = TOKEN_TO_STRING[token];
|
|
||||||
if (tokenString === 'PRINT') {
|
if (tokenString === 'PRINT') {
|
||||||
tokenString = '?';
|
tokenString = '?';
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tokenString = LETTERS[token];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spaceIf(tokenString)) {
|
if (spaceIf(tokenString)) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
@ -239,13 +272,11 @@ export default class ApplesoftDecompiler {
|
|||||||
offset += 2;
|
offset += 2;
|
||||||
|
|
||||||
while (this.program[offset] !== 0) {
|
while (this.program[offset] !== 0) {
|
||||||
const token = this.program[offset];
|
if (offset >= this.program.length) {
|
||||||
let tokenString: string;
|
return 'Unterminated line: ' + result;
|
||||||
if (token >= 0x80 && token <= 0xea) {
|
|
||||||
tokenString = TOKEN_TO_STRING[token];
|
|
||||||
} else {
|
|
||||||
tokenString = LETTERS[token];
|
|
||||||
}
|
}
|
||||||
|
const token = this.program[offset];
|
||||||
|
const tokenString = resolveToken(token);
|
||||||
if (tokenString === '"') {
|
if (tokenString === '"') {
|
||||||
inString = !inString;
|
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;
|
export const ARYTAB = 0x6B;
|
||||||
/** End of strings (word). (Strings are allocated down from HIMEM.) */
|
/** End of strings (word). (Strings are allocated down from HIMEM.) */
|
||||||
export const STREND = 0x6D;
|
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
|
* End of program (word). This is actually 1 or 2 bytes past the three
|
||||||
* zero bytes that end the program.
|
* 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;
|
const data = this.imageData.data;
|
||||||
let dx, dy;
|
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;
|
let y = rowa << 4 | rowb << 1;
|
||||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||||
y += 1;
|
y += 1;
|
||||||
@ -916,6 +916,14 @@ export class VideoModes2D implements VideoModes {
|
|||||||
this._hgrs[page - 1] = hires;
|
this._hgrs[page - 1] = hires;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLoresPage(page: pageNo) {
|
||||||
|
return this._grs[page - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiresPage(page: pageNo) {
|
||||||
|
return this._hgrs[page - 1];
|
||||||
|
}
|
||||||
|
|
||||||
text(on: boolean) {
|
text(on: boolean) {
|
||||||
const old = this.textMode;
|
const old = this.textMode;
|
||||||
this.textMode = on;
|
this.textMode = on;
|
||||||
|
@ -18,10 +18,10 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
|||||||
private bank2: RAM;
|
private bank2: RAM;
|
||||||
private ram: RAM;
|
private ram: RAM;
|
||||||
|
|
||||||
private readbsr = false;
|
private _readbsr = false;
|
||||||
private writebsr = false;
|
private _writebsr = false;
|
||||||
private bsr2 = false;
|
private _bsr2 = false;
|
||||||
private prewrite = false;
|
private _prewrite = false;
|
||||||
|
|
||||||
private read1: Memory;
|
private read1: Memory;
|
||||||
private read2: Memory;
|
private read2: Memory;
|
||||||
@ -48,16 +48,16 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateBanks() {
|
private updateBanks() {
|
||||||
if (this.readbsr) {
|
if (this._readbsr) {
|
||||||
this.read1 = this.bsr2 ? this.bank2 : this.bank1;
|
this.read1 = this._bsr2 ? this.bank2 : this.bank1;
|
||||||
this.read2 = this.ram;
|
this.read2 = this.ram;
|
||||||
} else {
|
} else {
|
||||||
this.read1 = this.rom;
|
this.read1 = this.rom;
|
||||||
this.read2 = this.rom;
|
this.read2 = this.rom;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.writebsr) {
|
if (this._writebsr) {
|
||||||
this.write1 = this.bsr2 ? this.bank2 : this.bank1;
|
this.write1 = this._bsr2 ? this.bank2 : this.bank1;
|
||||||
this.write2 = this.ram;
|
this.write2 = this.ram;
|
||||||
} else {
|
} else {
|
||||||
this.write1 = this.rom;
|
this.write1 = this.rom;
|
||||||
@ -90,35 +90,35 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
|||||||
|
|
||||||
if (writeSwitch) { // $C081, $C083, $C089, $C08B
|
if (writeSwitch) { // $C081, $C083, $C089, $C08B
|
||||||
if (readMode) {
|
if (readMode) {
|
||||||
this.writebsr = this.prewrite;
|
this._writebsr = this._prewrite;
|
||||||
}
|
}
|
||||||
this.prewrite = readMode;
|
this._prewrite = readMode;
|
||||||
|
|
||||||
if (offSwitch) { // $C083, $C08B
|
if (offSwitch) { // $C083, $C08B
|
||||||
this.readbsr = true;
|
this._readbsr = true;
|
||||||
rwStr = 'Read/Write';
|
rwStr = 'Read/Write';
|
||||||
} else { // $C081, $C089
|
} else { // $C081, $C089
|
||||||
this.readbsr = false;
|
this._readbsr = false;
|
||||||
rwStr = 'Write';
|
rwStr = 'Write';
|
||||||
}
|
}
|
||||||
} else { // $C080, $C082, $C088, $C08A
|
} else { // $C080, $C082, $C088, $C08A
|
||||||
this.writebsr = false;
|
this._writebsr = false;
|
||||||
this.prewrite = false;
|
this._prewrite = false;
|
||||||
|
|
||||||
if (offSwitch) { // $C082, $C08A
|
if (offSwitch) { // $C082, $C08A
|
||||||
this.readbsr = false;
|
this._readbsr = false;
|
||||||
rwStr = 'Off';
|
rwStr = 'Off';
|
||||||
} else { // $C080, $C088
|
} else { // $C080, $C088
|
||||||
this.readbsr = true;
|
this._readbsr = true;
|
||||||
rwStr = 'Read';
|
rwStr = 'Read';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bank1Switch) { // C08[8-C]
|
if (bank1Switch) { // C08[8-C]
|
||||||
this.bsr2 = false;
|
this._bsr2 = false;
|
||||||
bankStr = 'Bank 1';
|
bankStr = 'Bank 1';
|
||||||
} else { // C08[0-3]
|
} else { // C08[0-3]
|
||||||
this.bsr2 = true;
|
this._bsr2 = true;
|
||||||
bankStr = 'Bank 2';
|
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() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
readbsr: this.readbsr,
|
readbsr: this.readbsr,
|
||||||
writebsr: this.writebsr,
|
writebsr: this.writebsr,
|
||||||
bsr2: this.bsr2,
|
bsr2: this.bsr2,
|
||||||
prewrite: this.prewrite,
|
prewrite: this._prewrite,
|
||||||
ram: this.ram.getState(),
|
ram: this.ram.getState(),
|
||||||
bank1: this.bank1.getState(),
|
bank1: this.bank1.getState(),
|
||||||
bank2: this.bank2.getState()
|
bank2: this.bank2.getState()
|
||||||
@ -171,10 +183,10 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(state: LanguageCardState) {
|
setState(state: LanguageCardState) {
|
||||||
this.readbsr = state.readbsr;
|
this._readbsr = state.readbsr;
|
||||||
this.writebsr = state.writebsr;
|
this._writebsr = state.writebsr;
|
||||||
this.bsr2 = state.bsr2;
|
this._bsr2 = state.bsr2;
|
||||||
this.prewrite = state.prewrite;
|
this._prewrite = state.prewrite;
|
||||||
this.ram.setState(state.ram);
|
this.ram.setState(state.ram);
|
||||||
this.bank1.setState(state.bank1);
|
this.bank1.setState(state.bank1);
|
||||||
this.bank2.setState(state.bank2);
|
this.bank2.setState(state.bank2);
|
||||||
|
@ -3,7 +3,7 @@ import cs from 'classnames';
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { Apple2 as Apple2Impl } from '../apple2';
|
import { Apple2 as Apple2Impl } from '../apple2';
|
||||||
import { ControlStrip } from './ControlStrip';
|
import { ControlStrip } from './ControlStrip';
|
||||||
import { Debugger } from './Debugger';
|
import { Debugger } from './debugger/Debugger';
|
||||||
import { ErrorModal } from './ErrorModal';
|
import { ErrorModal } from './ErrorModal';
|
||||||
import { Inset } from './Inset';
|
import { Inset } from './Inset';
|
||||||
import { Keyboard } from './Keyboard';
|
import { Keyboard } from './Keyboard';
|
||||||
@ -151,7 +151,7 @@ export const Apple2 = (props: Apple2Props) => {
|
|||||||
</Inset>
|
</Inset>
|
||||||
<ErrorModal error={error} setError={setError} />
|
<ErrorModal error={error} setError={setError} />
|
||||||
</div>
|
</div>
|
||||||
{showDebug ? <Debugger apple2={apple2} e={e} /> : null}
|
{showDebug ? <Debugger apple2={apple2} /> : null}
|
||||||
</div>
|
</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) {
|
if (document.activeElement && document.activeElement !== document.body) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
const key = mapKeyEvent(event, active.includes('LOCK'));
|
const key = mapKeyEvent(event, active.includes('LOCK'));
|
||||||
if (key !== 0xff) {
|
if (key !== 0xff) {
|
||||||
// CTRL-SHIFT-DELETE for reset
|
// 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 {
|
.inset {
|
||||||
margin: 5px 10px 0;
|
margin: 5px 10px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-bottom: 10px;
|
margin: 5px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
padding: 3px 0;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subHeading {
|
.subHeading {
|
||||||
@ -55,33 +26,29 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zeroPage {
|
.debugger {
|
||||||
width: 50em;
|
font-size: 12px;
|
||||||
|
width: 590px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace {
|
.debugger pre {
|
||||||
width: 50em;
|
font-size: 9px;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
padding: 3px;
|
||||||
|
margin: 2px;
|
||||||
|
border: 1px inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack {
|
.debugger button,
|
||||||
width: 10em;
|
.debugger input {
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileChooser {
|
.debugger input[type="text"] {
|
||||||
padding: 5px 0;
|
border: 1px inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid {
|
.debugger hr {
|
||||||
color: #f00;
|
color: #c4c1a0;
|
||||||
}
|
|
||||||
|
|
||||||
div.invalid {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.invalid i {
|
|
||||||
position: absolute;
|
|
||||||
top: -9px;
|
|
||||||
left: -16px;
|
|
||||||
}
|
}
|
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 {
|
public read(a: number, b?: number): byte {
|
||||||
let page, off;
|
let page, off;
|
||||||
if (b !== undefined) {
|
if (b !== undefined) {
|
||||||
page = a;
|
page = a & 0xff;
|
||||||
off = b;
|
off = b & 0xff;
|
||||||
} else {
|
} else {
|
||||||
page = a >> 8;
|
page = (a >> 8) & 0xff;
|
||||||
off = a & 0xff;
|
off = a & 0xff;
|
||||||
}
|
}
|
||||||
return this.memPages[page].read(page, off);
|
return this.memPages[page].read(page, off);
|
||||||
@ -1551,13 +1551,13 @@ export default class CPU6502 {
|
|||||||
let page, off, val;
|
let page, off, val;
|
||||||
|
|
||||||
if (c !== undefined ) {
|
if (c !== undefined ) {
|
||||||
page = a;
|
page = a & 0xff;
|
||||||
off = b;
|
off = b & 0xff;
|
||||||
val = c;
|
val = c & 0xff;
|
||||||
} else {
|
} else {
|
||||||
page = a >> 8;
|
page = (a >> 8) & 0xff;
|
||||||
off = a & 0xff;
|
off = a & 0xff;
|
||||||
val = b;
|
val = b & 0xff;
|
||||||
}
|
}
|
||||||
this.memPages[page].write(page, off, val);
|
this.memPages[page].write(page, off, val);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import CPU6502, { DebugInfo, flags, sizes } from './cpu6502';
|
|||||||
export interface DebuggerContainer {
|
export interface DebuggerContainer {
|
||||||
run: () => void;
|
run: () => void;
|
||||||
stop: () => void;
|
stop: () => void;
|
||||||
getCPU: () => CPU6502;
|
|
||||||
isRunning: () => boolean;
|
isRunning: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,16 +27,13 @@ export const dumpStatusRegister = (sr: byte) =>
|
|||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
export default class Debugger {
|
export default class Debugger {
|
||||||
private cpu: CPU6502;
|
|
||||||
private verbose = false;
|
private verbose = false;
|
||||||
private maxTrace = 256;
|
private maxTrace = 256;
|
||||||
private trace: DebugInfo[] = [];
|
private trace: DebugInfo[] = [];
|
||||||
private breakpoints: Map<word, breakpointFn> = new Map();
|
private breakpoints: Map<word, breakpointFn> = new Map();
|
||||||
private symbols: symbols = {};
|
private symbols: symbols = {};
|
||||||
|
|
||||||
constructor(private container: DebuggerContainer) {
|
constructor(private cpu: CPU6502, private container: DebuggerContainer) {}
|
||||||
this.cpu = container.getCPU();
|
|
||||||
}
|
|
||||||
|
|
||||||
stepCycles(cycles: number) {
|
stepCycles(cycles: number) {
|
||||||
this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => {
|
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;
|
this._hgrs[page - 1] = hires;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLoresPage(page: pageNo) {
|
||||||
|
return this._grs[page - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiresPage(page: pageNo) {
|
||||||
|
return this._hgrs[page - 1];
|
||||||
|
}
|
||||||
|
|
||||||
text(on: boolean) {
|
text(on: boolean) {
|
||||||
const old = this.textMode;
|
const old = this.textMode;
|
||||||
this.textMode = on;
|
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;
|
private _altzp: boolean;
|
||||||
|
|
||||||
// Video
|
// Video
|
||||||
private _80store: boolean;
|
private __80store: boolean;
|
||||||
private _page2: boolean;
|
private _page2: boolean;
|
||||||
private _hires: boolean;
|
private _hires: boolean;
|
||||||
|
|
||||||
@ -289,7 +289,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSwitches() {
|
private _initSwitches() {
|
||||||
this._bank1 = false;
|
this._bank1 = false;
|
||||||
this._readbsr = false;
|
this._readbsr = false;
|
||||||
this._writebsr = false;
|
this._writebsr = false;
|
||||||
@ -303,14 +303,14 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
this._slot3rom = false;
|
this._slot3rom = false;
|
||||||
this._intc8rom = false;
|
this._intc8rom = false;
|
||||||
|
|
||||||
this._80store = false;
|
this.__80store = false;
|
||||||
this._page2 = false;
|
this._page2 = false;
|
||||||
this._hires = false;
|
this._hires = false;
|
||||||
|
|
||||||
this._iouDisable = true;
|
this._iouDisable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_debug(..._args: unknown[]) {
|
private _debug(..._args: unknown[]) {
|
||||||
// debug.apply(this, _args);
|
// 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) {
|
if (this._page2) {
|
||||||
for (let idx = 0x4; idx < 0x8; idx++) {
|
for (let idx = 0x4; idx < 0x8; idx++) {
|
||||||
this._readPages[idx] = this._pages[idx][1];
|
this._readPages[idx] = this._pages[idx][1];
|
||||||
@ -440,15 +440,15 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
|
|
||||||
// Apple //e memory management
|
// Apple //e memory management
|
||||||
|
|
||||||
_accessMMUSet(off: byte, _val?: byte) {
|
private _accessMMUSet(off: byte, _val?: byte) {
|
||||||
switch (off) {
|
switch (off) {
|
||||||
case LOC._80STOREOFF:
|
case LOC._80STOREOFF:
|
||||||
this._80store = false;
|
this.__80store = false;
|
||||||
this._debug('80 Store Off', _val);
|
this._debug('80 Store Off', _val);
|
||||||
this.vm.page(this._page2 ? 2 : 1);
|
this.vm.page(this._page2 ? 2 : 1);
|
||||||
break;
|
break;
|
||||||
case LOC._80STOREON:
|
case LOC._80STOREON:
|
||||||
this._80store = true;
|
this.__80store = true;
|
||||||
this._debug('80 Store On', _val);
|
this._debug('80 Store On', _val);
|
||||||
break;
|
break;
|
||||||
case LOC.RAMRDOFF:
|
case LOC.RAMRDOFF:
|
||||||
@ -520,7 +520,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
|
|
||||||
// Status registers
|
// Status registers
|
||||||
|
|
||||||
_accessStatus(off: byte, val?: byte) {
|
private _accessStatus(off: byte, val?: byte) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
|
|
||||||
switch(off) {
|
switch(off) {
|
||||||
@ -553,8 +553,8 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
result = this._slot3rom ? 0x80 : 0x00;
|
result = this._slot3rom ? 0x80 : 0x00;
|
||||||
break;
|
break;
|
||||||
case LOC._80STORE: // 0xC018
|
case LOC._80STORE: // 0xC018
|
||||||
this._debug(`80 Store ${this._80store ? 'true' : 'false'}`);
|
this._debug(`80 Store ${this.__80store ? 'true' : 'false'}`);
|
||||||
result = this._80store ? 0x80 : 0x00;
|
result = this.__80store ? 0x80 : 0x00;
|
||||||
break;
|
break;
|
||||||
case LOC.VERTBLANK: // 0xC019
|
case LOC.VERTBLANK: // 0xC019
|
||||||
// result = cpu.getCycles() % 20 < 5 ? 0x80 : 0x00;
|
// result = cpu.getCycles() % 20 < 5 ? 0x80 : 0x00;
|
||||||
@ -585,7 +585,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_accessIOUDisable(off: byte, val?: byte) {
|
private _accessIOUDisable(off: byte, val?: byte) {
|
||||||
const writeMode = val !== undefined;
|
const writeMode = val !== undefined;
|
||||||
let result;
|
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;
|
let result: byte | undefined = 0;
|
||||||
|
|
||||||
switch (off) {
|
switch (off) {
|
||||||
case LOC.PAGE1:
|
case LOC.PAGE1:
|
||||||
this._page2 = false;
|
this._page2 = false;
|
||||||
if (!this._80store) {
|
if (!this.__80store) {
|
||||||
result = this.io.ioSwitch(off, val);
|
result = this.io.ioSwitch(off, val);
|
||||||
}
|
}
|
||||||
this._debug('Page 2 off');
|
this._debug('Page 2 off');
|
||||||
@ -628,7 +628,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
|
|
||||||
case LOC.PAGE2:
|
case LOC.PAGE2:
|
||||||
this._page2 = true;
|
this._page2 = true;
|
||||||
if (!this._80store) {
|
if (!this.__80store) {
|
||||||
result = this.io.ioSwitch(off, val);
|
result = this.io.ioSwitch(off, val);
|
||||||
}
|
}
|
||||||
this._debug('Page 2 on');
|
this._debug('Page 2 on');
|
||||||
@ -671,7 +671,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_accessLangCard(off: byte, val?: byte) {
|
private _accessLangCard(off: byte, val?: byte) {
|
||||||
const readMode = val === undefined;
|
const readMode = val === undefined;
|
||||||
const result = readMode ? 0 : undefined;
|
const result = readMode ? 0 : undefined;
|
||||||
|
|
||||||
@ -801,6 +801,46 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
this._vbEnd = this.cpu.getCycles() + 1000;
|
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 {
|
public getState(): MMUState {
|
||||||
return {
|
return {
|
||||||
bank1: this._bank1,
|
bank1: this._bank1,
|
||||||
@ -816,7 +856,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
auxRamWrite: this._auxRamWrite,
|
auxRamWrite: this._auxRamWrite,
|
||||||
altzp: this._altzp,
|
altzp: this._altzp,
|
||||||
|
|
||||||
_80store: this._80store,
|
_80store: this.__80store,
|
||||||
page2: this._page2,
|
page2: this._page2,
|
||||||
hires: this._hires,
|
hires: this._hires,
|
||||||
|
|
||||||
@ -853,7 +893,7 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
|||||||
this._auxRamWrite = state.auxRamWrite;
|
this._auxRamWrite = state.auxRamWrite;
|
||||||
this._altzp = state.altzp;
|
this._altzp = state.altzp;
|
||||||
|
|
||||||
this._80store = state._80store;
|
this.__80store = state._80store;
|
||||||
this._page2 = state.page2;
|
this._page2 = state.page2;
|
||||||
this._hires = state.hires;
|
this._hires = state.hires;
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import Tape, { TAPE_TYPES } from './tape';
|
|||||||
|
|
||||||
import ApplesoftDecompiler from '../applesoft/decompiler';
|
import ApplesoftDecompiler from '../applesoft/decompiler';
|
||||||
import ApplesoftCompiler from '../applesoft/compiler';
|
import ApplesoftCompiler from '../applesoft/compiler';
|
||||||
|
import { TXTTAB } from 'js/applesoft/zeropage';
|
||||||
|
|
||||||
import { debug } from '../util';
|
import { debug } from '../util';
|
||||||
import { Apple2, Stats, State as Apple2State } from '../apple2';
|
import { Apple2, Stats, State as Apple2State } from '../apple2';
|
||||||
@ -90,18 +91,15 @@ let ready: Promise<[void, void]>;
|
|||||||
|
|
||||||
export const driveLights = new DriveLights();
|
export const driveLights = new DriveLights();
|
||||||
|
|
||||||
/** Start of program (word) */
|
export function dumpApplesoftProgram() {
|
||||||
const TXTTAB = 0x67;
|
|
||||||
|
|
||||||
export function dumpAppleSoftProgram() {
|
|
||||||
const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu);
|
const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu);
|
||||||
debug(decompiler.list({apple2: _e ? 'e' : 'plus'}));
|
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);
|
const start = cpu.read(TXTTAB) + (cpu.read(TXTTAB + 1) << 8);
|
||||||
ApplesoftCompiler.compileToMemory(cpu, program, start);
|
ApplesoftCompiler.compileToMemory(cpu, program, start);
|
||||||
dumpAppleSoftProgram();
|
dumpApplesoftProgram();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openLoad(driveString: string, event: MouseEvent) {
|
export function openLoad(driveString: string, event: MouseEvent) {
|
||||||
|
@ -67,7 +67,9 @@ export interface VideoModes extends Restorable<VideoModesState> {
|
|||||||
reset(): void;
|
reset(): void;
|
||||||
|
|
||||||
setLoresPage(page: pageNo, lores: LoresPage): void;
|
setLoresPage(page: pageNo, lores: LoresPage): void;
|
||||||
|
getLoresPage(page: pageNo): LoresPage;
|
||||||
setHiresPage(page: pageNo, lores: HiresPage): void;
|
setHiresPage(page: pageNo, lores: HiresPage): void;
|
||||||
|
getHiresPage(page: pageNo): HiresPage;
|
||||||
|
|
||||||
_80col(on: boolean): void;
|
_80col(on: boolean): void;
|
||||||
altChar(on: boolean): void;
|
altChar(on: boolean): void;
|
||||||
|
@ -18,12 +18,11 @@ describe('Debugger', () => {
|
|||||||
cpu.addPageHandler(bios);
|
cpu.addPageHandler(bios);
|
||||||
|
|
||||||
debuggerContainer = {
|
debuggerContainer = {
|
||||||
getCPU: () => cpu,
|
|
||||||
run: jest.fn(),
|
run: jest.fn(),
|
||||||
stop: jest.fn(),
|
stop: jest.fn(),
|
||||||
isRunning: jest.fn(),
|
isRunning: jest.fn(),
|
||||||
};
|
};
|
||||||
theDebugger = new Debugger(debuggerContainer);
|
theDebugger = new Debugger(cpu, debuggerContainer);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#utility', () => {
|
describe('#utility', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user