@fugloy/emu 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +235 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3350 -0
- package/src/debug/debugger.js +788 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +429 -0
- package/src/debug/state.js +273 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +263 -0
- package/src/devices/disk-image.js +965 -0
- package/src/devices/fdc-wd1771.js +180 -0
- package/src/devices/fdc-wd1793.js +1065 -0
- package/src/devices/interrupt-latch.js +76 -0
- package/src/devices/key-queue.js +278 -0
- package/src/devices/keyboard.js +176 -0
- package/src/devices/printer.js +44 -0
- package/src/devices/sound.js +53 -0
- package/src/devices/video.js +69 -0
- package/src/index.js +23 -0
- package/src/loaders/bas.js +107 -0
- package/src/loaders/cas-format.js +213 -0
- package/src/loaders/cmd-format.js +88 -0
- package/src/loaders/handoff.js +29 -0
- package/src/loaders/load.js +299 -0
- package/src/machine/drive-select.js +42 -0
- package/src/machine/io.js +513 -0
- package/src/machine/machine.js +333 -0
- package/src/machine/memory.js +290 -0
- package/src/machine/models.js +123 -0
- package/src/machine/rom-hashes.js +21 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +289 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fugloy/emu — headless, deterministic TRS-80 Model III and Model I emulator core.
|
|
3
|
+
* Extracted in emu phase 0 from cschweda/trs80-emulator at 11612849 (v1.6.0); see NOTICE.md.
|
|
4
|
+
*/
|
|
5
|
+
export { Machine } from './machine/machine.js';
|
|
6
|
+
export { Debugger, COVERAGE } from './debug/debugger.js';
|
|
7
|
+
export { LABEL_FILE_VERSION, COVERAGE_FIELDS, encodeRle, decodeRle, parseLabelFile, mergeLabelFiles } from './debug/label-file.js';
|
|
8
|
+
export { ROM_HASHES } from './machine/rom-hashes.js';
|
|
9
|
+
export { clockHz } from './machine/models.js';
|
|
10
|
+
export { TRS80System, CPU_CLOCK_HZ, RTC_HZ, RTC_INTERVAL_TSTATES } from './machine/trs80-system.js';
|
|
11
|
+
export { Z80CPU } from './cpu/z80cpu.js';
|
|
12
|
+
export { MemorySystem } from './machine/memory.js';
|
|
13
|
+
export { IOSystem, RTC_IRQ_BIT, CASSETTE_RISE_IRQ_BIT, CASSETTE_FALL_IRQ_BIT, NMI_INTRQ_BIT, MODE_32COL_BIT } from './machine/io.js';
|
|
14
|
+
export { KeyboardMatrix } from './devices/keyboard.js';
|
|
15
|
+
export { CassetteSystem } from './devices/cassette.js';
|
|
16
|
+
export { casToPulses, pulsesToBytes, ensureLeader } from './devices/cassette-tape.js';
|
|
17
|
+
export { FDC1793, parseWriteTrackStream } from './devices/fdc-wd1793.js';
|
|
18
|
+
export { DiskImage, JV1_MODEL1_SD_TRACKS } from './devices/disk-image.js';
|
|
19
|
+
export { Printer } from './devices/printer.js';
|
|
20
|
+
export { parseCas, fastLoadBasic, fastLoadSystem } from './loaders/cas-format.js';
|
|
21
|
+
export { parseBas } from './loaders/bas.js';
|
|
22
|
+
export { parseCmd, fastLoadCmd } from './loaders/cmd-format.js';
|
|
23
|
+
export { STATE_VERSION, STATE_MACHINE, toBase64, fromBase64, serializeState, restoreState } from './debug/state.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .BAS loader. A first byte of $FF means a tokenized program: per line a 2-byte next pointer, a
|
|
3
|
+
* 2-byte line number, the body, and 00, with two zero bytes ending the program. Otherwise the
|
|
4
|
+
* file is ASCII source, which the Machine feeds through the ROM keyboard hook.
|
|
5
|
+
*
|
|
6
|
+
* The pointers in a tokenized file are absolute from the machine that saved it, so they are
|
|
7
|
+
* rebuilt for the load address. A line body may hold 00 (machine code in a REM), so the chain is
|
|
8
|
+
* followed by pointer when it is self-consistent and by 00-scan when it is not.
|
|
9
|
+
*/
|
|
10
|
+
import { writeTokenizedProgram } from './cas-format.js';
|
|
11
|
+
|
|
12
|
+
const hex = (n) => `$${n.toString(16).toUpperCase()}`;
|
|
13
|
+
|
|
14
|
+
/** Follows the pointer chain with line 1 assumed at `base`; null when it does not hold together. */
|
|
15
|
+
function walk(bytes, start, base) {
|
|
16
|
+
const n = bytes.length;
|
|
17
|
+
const lines = [];
|
|
18
|
+
let off = start;
|
|
19
|
+
let addr = base;
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (off + 2 > n) return null;
|
|
22
|
+
const ptr = bytes[off] | (bytes[off + 1] << 8);
|
|
23
|
+
if (ptr === 0) return lines;
|
|
24
|
+
const len = ptr - addr;
|
|
25
|
+
if (len < 5 || off + len > n || bytes[off + len - 1] !== 0) return null;
|
|
26
|
+
lines.push({ lineNo: bytes[off + 2] | (bytes[off + 3] << 8), tokens: bytes.slice(off + 4, off + len - 1) });
|
|
27
|
+
addr = ptr;
|
|
28
|
+
off += len;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Splits at each 00, ignoring the pointers; throws when the program runs off the end of the file. */
|
|
33
|
+
function scan(bytes, start) {
|
|
34
|
+
const n = bytes.length;
|
|
35
|
+
const lines = [];
|
|
36
|
+
let off = start;
|
|
37
|
+
for (;;) {
|
|
38
|
+
if (off + 2 > n) throw new Error(`truncated .BAS: no 00 00 program terminator after ${lines.length} lines`);
|
|
39
|
+
if (bytes[off] === 0 && bytes[off + 1] === 0) return lines;
|
|
40
|
+
if (off + 4 > n) throw new Error(`truncated .BAS: line header cut off at offset ${off}`);
|
|
41
|
+
const end = bytes.indexOf(0, off + 4);
|
|
42
|
+
if (end < 0) throw new Error(`truncated .BAS: line at offset ${off} has no 00 terminator`);
|
|
43
|
+
lines.push({ lineNo: bytes[off + 2] | (bytes[off + 3] << 8), tokens: bytes.slice(off + 4, end) });
|
|
44
|
+
off = end + 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseTokenized(bytes) {
|
|
49
|
+
const start = 1;
|
|
50
|
+
if (bytes.length - start < 2) throw new Error('truncated .BAS: no program after the $FF header');
|
|
51
|
+
const ptr1 = bytes[start] | (bytes[start + 1] << 8);
|
|
52
|
+
if (ptr1 === 0) return { kind: 'tokenized', lines: [] };
|
|
53
|
+
// Line 1's base is unknown: try the stock TXTTAB, then each 00 in line 1 as its end.
|
|
54
|
+
const bases = [0x42e9];
|
|
55
|
+
for (let j = start + 4; j < bytes.length; j++) {
|
|
56
|
+
if (bytes[j] === 0) {
|
|
57
|
+
bases.push(ptr1 - (j + 1 - start));
|
|
58
|
+
if (bases.length > 64) break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const base of bases) {
|
|
62
|
+
const lines = walk(bytes, start, base);
|
|
63
|
+
if (lines) return { kind: 'tokenized', lines };
|
|
64
|
+
}
|
|
65
|
+
// No pointer chain holds together (a bad next pointer, say): fall back to splitting at each 00,
|
|
66
|
+
// which throws "truncated" only when the program runs off the end of the file.
|
|
67
|
+
return { kind: 'tokenized', lines: scan(bytes, start) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a .BAS file.
|
|
72
|
+
* @param {Uint8Array} bytes
|
|
73
|
+
* @returns {{ kind: 'tokenized', lines: { lineNo: number, tokens: Uint8Array }[] } | { kind: 'ascii', text: string }}
|
|
74
|
+
*/
|
|
75
|
+
export function parseBas(bytes) {
|
|
76
|
+
if (!bytes || bytes.length === 0) throw new Error('not a .BAS file: empty');
|
|
77
|
+
if (bytes[0] === 0xff) return parseTokenized(bytes);
|
|
78
|
+
let text = '';
|
|
79
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
80
|
+
const b = bytes[i];
|
|
81
|
+
if (b === 0) throw new Error(`not a .BAS file: NUL at offset ${i} (looks like tokenized BASIC without the $FF header; add a leading $FF byte to load it)`);
|
|
82
|
+
if (!((b >= 0x20 && b <= 0x7e) || b === 0x0d || b === 0x0a || b === 0x09)) {
|
|
83
|
+
throw new Error(`not a .BAS file: byte ${hex(b)} at offset ${i} is neither tokenized ($FF first) nor ASCII text`);
|
|
84
|
+
}
|
|
85
|
+
text += String.fromCharCode(b);
|
|
86
|
+
}
|
|
87
|
+
text = text.replace(/\r\n?/g, '\n');
|
|
88
|
+
if (!text.endsWith('\n')) text += '\n';
|
|
89
|
+
return { kind: 'ascii', text };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load a .BAS file into a Machine. Tokenized: write at TXTTAB and fix the pointers. ASCII: feed
|
|
94
|
+
* the text through the keyboard hook, which BASIC reads as the machine runs.
|
|
95
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
96
|
+
* @param {Uint8Array} bytes
|
|
97
|
+
* @returns {{ format: 'bas', kind: 'tokenized' | 'ascii', start: number, end: number | null, blocks: { addr: number, len: number, why: string }[] }}
|
|
98
|
+
*/
|
|
99
|
+
export function loadBas(machine, bytes) {
|
|
100
|
+
const parsed = parseBas(bytes);
|
|
101
|
+
if (parsed.kind === 'ascii') {
|
|
102
|
+
machine.keyboard.feed(parsed.text);
|
|
103
|
+
return { format: 'bas', kind: 'ascii', start: machine.system.memory.readWord(0x40a4), end: null, blocks: [] };
|
|
104
|
+
}
|
|
105
|
+
const { start, end } = writeTokenizedProgram(machine.system, parsed.lines);
|
|
106
|
+
return { format: 'bas', kind: 'tokenized', start, end, blocks: [{ addr: start, len: end - start, why: 'BASIC program' }] };
|
|
107
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 .cas cassette image handling
|
|
3
|
+
*
|
|
4
|
+
* A .cas file is the decoded byte stream of a tape. Two layouts matter:
|
|
5
|
+
*
|
|
6
|
+
* BASIC (CSAVE): [leader 0x00/0x55 ...] 0xA5 0xD3 0xD3 0xD3 name(1)
|
|
7
|
+
* then the tokenized program image: per line
|
|
8
|
+
* [link u16][line# u16][tokens...][0x00], ending with a
|
|
9
|
+
* 0x0000 link. Link pointers are absolute addresses from
|
|
10
|
+
* the saving machine — they are recomputed on load.
|
|
11
|
+
*
|
|
12
|
+
* SYSTEM: [leader] 0xA5 0x55 name(6) then blocks:
|
|
13
|
+
* 0x3C len(0=256) addr(u16) data[len] checksum
|
|
14
|
+
* terminated by 0x78 entry(u16).
|
|
15
|
+
*
|
|
16
|
+
* Fast-loading bypasses the cassette port entirely: BASIC programs are
|
|
17
|
+
* written at TXTTAB with fresh links and the variable pointers fixed up
|
|
18
|
+
* (what CLOAD leaves behind); SYSTEM blocks are written to their target
|
|
19
|
+
* addresses and execution jumps to the entry point (what SYSTEM's "/"
|
|
20
|
+
* does).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { startProgram } from "./handoff.js";
|
|
24
|
+
|
|
25
|
+
// Model III cassette BASIC work area
|
|
26
|
+
const TXTTAB_PTR = 0x40a4; // -> start of BASIC program (0x42E9 stock)
|
|
27
|
+
const VARTAB_PTR = 0x40f9; // simple variables (= end of program)
|
|
28
|
+
const ARYTAB_PTR = 0x40fb; // arrays
|
|
29
|
+
const FRETOP_PTR = 0x40fd; // free space
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a .cas byte stream.
|
|
33
|
+
* @returns {{kind:"basic",name:string,lines:{lineNo:number,tokens:number[]}[]}
|
|
34
|
+
* | {kind:"system",name:string,blocks:{addr:number,data:Uint8Array}[],entry:number,checksumErrors:number}}
|
|
35
|
+
*/
|
|
36
|
+
export function parseCas(bytes) {
|
|
37
|
+
if (!bytes || bytes.length < 4) {
|
|
38
|
+
throw new Error("Not a .cas image: too short");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Skip the leader (0x00 or 0x55 bytes) up to the sync byte — 0xA5 on
|
|
42
|
+
// 500-baud tapes, 0x7F on Model III 1500-baud ones. Some images omit
|
|
43
|
+
// the leader or even the sync.
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < bytes.length && (bytes[i] === 0x00 || bytes[i] === 0x55)) i++;
|
|
46
|
+
if (bytes[i] === 0xa5 || bytes[i] === 0x7f) {
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
i = 0; // no leader/sync: start over at the raw content
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (bytes[i] === 0xd3 && bytes[i + 1] === 0xd3 && bytes[i + 2] === 0xd3) {
|
|
53
|
+
return parseBasic(bytes, i + 3);
|
|
54
|
+
}
|
|
55
|
+
if (bytes[i] === 0x55) {
|
|
56
|
+
return parseSystem(bytes, i + 1);
|
|
57
|
+
}
|
|
58
|
+
// A few BASIC images begin at the D3s without sync handled above
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Unrecognized .cas layout (expected BASIC 0xD3 0xD3 0xD3 or SYSTEM 0x55 header)"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Stock cassette BASIC program base (Model I and III alike): the link
|
|
65
|
+
// addresses a real CSAVE writes are absolute from here.
|
|
66
|
+
const TXTTAB_STOCK = 0x42e9;
|
|
67
|
+
|
|
68
|
+
function parseBasic(bytes, offset) {
|
|
69
|
+
const name = String.fromCharCode(bytes[offset]);
|
|
70
|
+
let i = offset + 1;
|
|
71
|
+
const lines = [];
|
|
72
|
+
|
|
73
|
+
// Real CLOAD walks the link-pointer chain, which is what lets a line
|
|
74
|
+
// contain 0x00 bytes (machine code embedded in a REM — the
|
|
75
|
+
// Christopherson games). Trust each link while it stays
|
|
76
|
+
// self-consistent; fall back to 0x00-scanning for that record when it
|
|
77
|
+
// doesn't (some tools write garbage links). After a fallback the next
|
|
78
|
+
// record re-syncs on the link value, so lengths derive from link
|
|
79
|
+
// deltas and the saving machine's base stops mattering.
|
|
80
|
+
let addr = TXTTAB_STOCK;
|
|
81
|
+
while (i + 1 < bytes.length) {
|
|
82
|
+
const link = bytes[i] | (bytes[i + 1] << 8);
|
|
83
|
+
if (link === 0x0000) {
|
|
84
|
+
break; // program terminator
|
|
85
|
+
}
|
|
86
|
+
const lineNo = bytes[i + 2] | (bytes[i + 3] << 8);
|
|
87
|
+
const recLen = link - addr; // [link u16][line# u16][tokens...][0x00]
|
|
88
|
+
if (
|
|
89
|
+
recLen >= 5 &&
|
|
90
|
+
recLen <= 1000 &&
|
|
91
|
+
i + recLen < bytes.length &&
|
|
92
|
+
bytes[i + recLen - 1] === 0x00
|
|
93
|
+
) {
|
|
94
|
+
lines.push({
|
|
95
|
+
lineNo,
|
|
96
|
+
tokens: Array.from(bytes.slice(i + 4, i + recLen - 1)),
|
|
97
|
+
});
|
|
98
|
+
i += recLen;
|
|
99
|
+
} else {
|
|
100
|
+
const tokens = [];
|
|
101
|
+
let j = i + 4;
|
|
102
|
+
while (j < bytes.length && bytes[j] !== 0x00) {
|
|
103
|
+
tokens.push(bytes[j]);
|
|
104
|
+
j++;
|
|
105
|
+
}
|
|
106
|
+
j++; // consume the end-of-line 0x00
|
|
107
|
+
lines.push({ lineNo, tokens });
|
|
108
|
+
i = j;
|
|
109
|
+
}
|
|
110
|
+
addr = link; // re-sync even after a fallback record
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { kind: "basic", name, lines };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseSystem(bytes, offset) {
|
|
117
|
+
const name = String.fromCharCode(...bytes.slice(offset, offset + 6))
|
|
118
|
+
.replace(/\0/g, " ")
|
|
119
|
+
.trimEnd();
|
|
120
|
+
let i = offset + 6;
|
|
121
|
+
const blocks = [];
|
|
122
|
+
let entry = null;
|
|
123
|
+
let checksumErrors = 0;
|
|
124
|
+
|
|
125
|
+
while (i < bytes.length) {
|
|
126
|
+
const marker = bytes[i++];
|
|
127
|
+
if (marker === 0x3c) {
|
|
128
|
+
let len = bytes[i++];
|
|
129
|
+
if (len === 0) len = 256;
|
|
130
|
+
const addr = bytes[i] | (bytes[i + 1] << 8);
|
|
131
|
+
i += 2;
|
|
132
|
+
const data = bytes.slice(i, i + len);
|
|
133
|
+
i += len;
|
|
134
|
+
const checksum = bytes[i++];
|
|
135
|
+
let sum = (addr & 0xff) + ((addr >> 8) & 0xff);
|
|
136
|
+
for (const b of data) sum += b;
|
|
137
|
+
if ((sum & 0xff) !== checksum) checksumErrors++;
|
|
138
|
+
blocks.push({ addr, data: new Uint8Array(data) });
|
|
139
|
+
} else if (marker === 0x78) {
|
|
140
|
+
entry = bytes[i] | (bytes[i + 1] << 8);
|
|
141
|
+
break;
|
|
142
|
+
} else if (marker === 0x00 || marker === 0x55) {
|
|
143
|
+
// stray leader/padding between blocks on some tapes
|
|
144
|
+
continue;
|
|
145
|
+
} else {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unexpected SYSTEM block marker 0x${marker.toString(16)} at ${i - 1}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (entry === null) {
|
|
153
|
+
throw new Error("SYSTEM tape has no entry-point (0x78) block");
|
|
154
|
+
}
|
|
155
|
+
return { kind: "system", name, blocks, entry, checksumErrors };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Write tokenized BASIC lines at TXTTAB with freshly computed next-line pointers, end the program
|
|
160
|
+
* with 00 00, and set the variable-area pointers ($40F9, $40FB, $40FD) as CLOAD leaves them.
|
|
161
|
+
* Shared by fastLoadBasic and the .BAS loader.
|
|
162
|
+
* @param {{ memory: any }} system
|
|
163
|
+
* @param {{ lineNo: number, tokens: ArrayLike<number> }[]} lines
|
|
164
|
+
* @returns {{ start: number, end: number }} TXTTAB, and the address one past the terminator
|
|
165
|
+
*/
|
|
166
|
+
export function writeTokenizedProgram(system, lines) {
|
|
167
|
+
const memory = system.memory;
|
|
168
|
+
const start = memory.readWord(TXTTAB_PTR);
|
|
169
|
+
let addr = start;
|
|
170
|
+
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
const next = addr + 2 + 2 + line.tokens.length + 1;
|
|
173
|
+
memory.writeWord(addr, next);
|
|
174
|
+
memory.writeWord(addr + 2, line.lineNo);
|
|
175
|
+
for (let i = 0; i < line.tokens.length; i++) {
|
|
176
|
+
memory.writeByte(addr + 4 + i, line.tokens[i]);
|
|
177
|
+
}
|
|
178
|
+
memory.writeByte(next - 1, 0x00);
|
|
179
|
+
addr = next;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
memory.writeWord(addr, 0x0000); // program terminator
|
|
183
|
+
addr += 2;
|
|
184
|
+
|
|
185
|
+
memory.writeWord(VARTAB_PTR, addr);
|
|
186
|
+
memory.writeWord(ARYTAB_PTR, addr);
|
|
187
|
+
memory.writeWord(FRETOP_PTR, addr);
|
|
188
|
+
return { start, end: addr };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fast-load a parsed BASIC program: write it at TXTTAB with recomputed
|
|
193
|
+
* line links and set the variable-area pointers, exactly the state CLOAD
|
|
194
|
+
* leaves. The machine should be sitting at READY. Caller typically
|
|
195
|
+
* follows with system.typeText("RUN\n").
|
|
196
|
+
* @returns {number} address one past the program terminator
|
|
197
|
+
*/
|
|
198
|
+
export function fastLoadBasic(system, parsed) {
|
|
199
|
+
return writeTokenizedProgram(system, parsed.lines).end;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Fast-load a parsed SYSTEM (machine-code) tape: write every block and
|
|
204
|
+
* jump to the entry point — the same transfer SYSTEM's "/" performs.
|
|
205
|
+
*/
|
|
206
|
+
export function fastLoadSystem(system, parsed) {
|
|
207
|
+
for (const block of parsed.blocks) {
|
|
208
|
+
for (let i = 0; i < block.data.length; i++) {
|
|
209
|
+
system.memory.writeByte((block.addr + i) & 0xffff, block.data[i]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return startProgram(system, parsed.entry);
|
|
213
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 DOS /CMD executable handling
|
|
3
|
+
*
|
|
4
|
+
* A /CMD file is a stream of records: [type][length][payload…]. The
|
|
5
|
+
* types honored here:
|
|
6
|
+
*
|
|
7
|
+
* 0x01 object-code block payload: addr.lo addr.hi data…
|
|
8
|
+
* For this type the length byte counts the
|
|
9
|
+
* address AND data bytes, with the quirk
|
|
10
|
+
* that 0, 1 and 2 mean 256, 257 and 258
|
|
11
|
+
* (a block always carries the 2 address
|
|
12
|
+
* bytes plus at least one data byte).
|
|
13
|
+
* 0x02 transfer address payload: entry.lo entry.hi — ends the load
|
|
14
|
+
* 0x05 module name payload: ASCII name
|
|
15
|
+
* other skipped by their length byte (comments,
|
|
16
|
+
* patch and directory records)
|
|
17
|
+
*
|
|
18
|
+
* Record 07 is a patch name header: its payload is the patch's name in
|
|
19
|
+
* ASCII, and the patch's code follows it as ordinary 01 blocks, which
|
|
20
|
+
* load like any other; a patch that has been YANKed has those blocks
|
|
21
|
+
* retyped 10, which the loader ignores (LDOS 5.1 manual, Tech Info:
|
|
22
|
+
* File Formats p. 1, item c; Soltoff, The Programmer's Guide to
|
|
23
|
+
* LDOS/TRSDOS Version 6, p. A-149, figure A-1). So skipping 07 and 10
|
|
24
|
+
* by their length is the whole of their handling.
|
|
25
|
+
*
|
|
26
|
+
* Fast-loading writes the blocks straight into RAM and jumps to the
|
|
27
|
+
* entry point — the same hand-off TRSDOS performs — so tape/disk-era
|
|
28
|
+
* machine-language games run with no DOS resident.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { startProgram } from "./handoff.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a /CMD byte stream.
|
|
35
|
+
* @returns {{name:string, blocks:{addr:number,data:Uint8Array}[], entry:number}}
|
|
36
|
+
*/
|
|
37
|
+
export function parseCmd(bytes) {
|
|
38
|
+
if (!bytes || bytes.length < 4) {
|
|
39
|
+
throw new Error("Not a /CMD image: too short");
|
|
40
|
+
}
|
|
41
|
+
const blocks = [];
|
|
42
|
+
let entry = null;
|
|
43
|
+
let name = "";
|
|
44
|
+
let i = 0;
|
|
45
|
+
|
|
46
|
+
while (i + 1 < bytes.length) {
|
|
47
|
+
const type = bytes[i++];
|
|
48
|
+
let len = bytes[i++];
|
|
49
|
+
if (type === 0x01) {
|
|
50
|
+
if (len < 3) len += 256; // 0,1,2 mean 256,257,258
|
|
51
|
+
if (i + len > bytes.length) {
|
|
52
|
+
throw new Error("Truncated /CMD load block");
|
|
53
|
+
}
|
|
54
|
+
const addr = bytes[i] | (bytes[i + 1] << 8);
|
|
55
|
+
blocks.push({ addr, data: new Uint8Array(bytes.slice(i + 2, i + len)) });
|
|
56
|
+
i += len;
|
|
57
|
+
} else if (type === 0x02) {
|
|
58
|
+
entry = bytes[i] | (bytes[i + 1] << 8);
|
|
59
|
+
break; // transfer address ends the load; trailing bytes are ignored
|
|
60
|
+
} else if (type === 0x05) {
|
|
61
|
+
name = String.fromCharCode(...bytes.slice(i, i + len)).trimEnd();
|
|
62
|
+
i += len;
|
|
63
|
+
} else {
|
|
64
|
+
i += len; // unknown record: skip its payload
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entry === null) {
|
|
69
|
+
throw new Error("/CMD image has no transfer-address (0x02) record");
|
|
70
|
+
}
|
|
71
|
+
if (blocks.length === 0) {
|
|
72
|
+
throw new Error("/CMD image has no load blocks");
|
|
73
|
+
}
|
|
74
|
+
return { name, blocks, entry };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fast-load a parsed /CMD: write every block and jump to the entry point.
|
|
79
|
+
*/
|
|
80
|
+
export function fastLoadCmd(system, parsed) {
|
|
81
|
+
for (const block of parsed.blocks) {
|
|
82
|
+
for (let i = 0; i < block.data.length; i++) {
|
|
83
|
+
system.memory.writeByte((block.addr + i) & 0xffff, block.data[i]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return startProgram(system, parsed.entry);
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loaders' hand-off to a loaded program, shared by fastLoadCmd (cmd-format.js) and
|
|
3
|
+
* fastLoadSystem (cas-format.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Jump to `entry` with nothing of the interrupted code left half-done. The CPU leaves HALT. An EI
|
|
8
|
+
* still in flight (it enables interrupts only after the next instruction) takes effect now, so
|
|
9
|
+
* IFF1 and IFF2 are set as they would be one instruction later; a DD/FD prefix still pending is
|
|
10
|
+
* dropped. Any interrupt request latched before the load is cleared, so no request from before
|
|
11
|
+
* the load interrupts the program; the next request is the next one the hardware raises.
|
|
12
|
+
* Otherwise IFF1, IFF2, and the interrupt mode are left as they were.
|
|
13
|
+
* @param {any} system a TRS80System (a bare { cpu } works too)
|
|
14
|
+
* @param {number} entry
|
|
15
|
+
* @returns {number} the entry
|
|
16
|
+
*/
|
|
17
|
+
export function startProgram(system, entry) {
|
|
18
|
+
const cpu = system.cpu;
|
|
19
|
+
cpu.halted = false;
|
|
20
|
+
if (cpu.pendingEI > 0) {
|
|
21
|
+
cpu.IFF1 = true;
|
|
22
|
+
cpu.IFF2 = true;
|
|
23
|
+
}
|
|
24
|
+
cpu.pendingEI = 0;
|
|
25
|
+
cpu.afterPrefix = false;
|
|
26
|
+
if (system.io) system.io.intLatch = 0;
|
|
27
|
+
cpu.registers.PC = entry;
|
|
28
|
+
return entry;
|
|
29
|
+
}
|