@fugloy/emu 0.0.0-stage → 0.2.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 +119 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3343 -0
- package/src/debug/debugger.js +777 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +423 -0
- package/src/debug/state.js +214 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +259 -0
- package/src/devices/disk-image.js +578 -0
- package/src/devices/fdc-wd1793.js +466 -0
- package/src/devices/key-queue.js +261 -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 +66 -0
- package/src/index.js +22 -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 +135 -0
- package/src/machine/io.js +290 -0
- package/src/machine/machine.js +295 -0
- package/src/machine/memory.js +207 -0
- package/src/machine/rom-hashes.js +14 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +232 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec §3 `m.keyboard`: a cycle-timed key queue over the KeyboardMatrix, and the feed buffer
|
|
3
|
+
* the ROM keyboard-scan hook (spec §6) reads.
|
|
4
|
+
*
|
|
5
|
+
* Matrix mode presses and releases matrix keys at T-state timestamps, as a typist would; any
|
|
6
|
+
* program sees them, the ROM and games that scan $3800 directly alike. Hook mode hands characters
|
|
7
|
+
* to the ROM's own scan entries ($002B, $0049) without touching the matrix, so it is fast and
|
|
8
|
+
* exact but reaches only code that calls the ROM. The Machine drains the queue at instruction
|
|
9
|
+
* boundaries and runs the hook there.
|
|
10
|
+
*/
|
|
11
|
+
import { CPU_CLOCK_HZ } from '../machine/trs80-system.js';
|
|
12
|
+
|
|
13
|
+
/** Key names → the KeyboardMatrix's browser-key names. */
|
|
14
|
+
const NAMES = {
|
|
15
|
+
ENTER: 'Enter',
|
|
16
|
+
CLEAR: 'Clear',
|
|
17
|
+
BREAK: 'Escape',
|
|
18
|
+
UP: 'ArrowUp',
|
|
19
|
+
DOWN: 'ArrowDown',
|
|
20
|
+
LEFT: 'ArrowLeft',
|
|
21
|
+
RIGHT: 'ArrowRight',
|
|
22
|
+
SHIFT: 'Shift',
|
|
23
|
+
SPACE: ' ',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The KeyboardMatrix key for a character or key name. The matrix maps browser keys, where an
|
|
28
|
+
* uppercase letter is physically shifted; on the Model III an unshifted letter key types the
|
|
29
|
+
* capital, so 'A' presses the bare A key and 'a' presses SHIFT+A.
|
|
30
|
+
* @param {string} key
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function matrixKey(key) {
|
|
34
|
+
if (Object.hasOwn(NAMES, key)) return NAMES[key];
|
|
35
|
+
if (key === '\n') return 'Enter';
|
|
36
|
+
if (/^[A-Z]$/.test(key)) return key.toLowerCase();
|
|
37
|
+
if (/^[a-z]$/.test(key)) return key.toUpperCase();
|
|
38
|
+
return key;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The byte the ROM's keyboard routine returns for a fed character: its ASCII code, with '\n'
|
|
43
|
+
* as ENTER ($0D).
|
|
44
|
+
* @param {string} ch
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
function romCode(ch) {
|
|
48
|
+
if (ch === '\n') return 0x0d;
|
|
49
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
50
|
+
if (ch.length !== 1 || code < 0x01 || code > 0x7f) throw new Error(`feed: ${JSON.stringify(ch)} is not a character the ROM keyboard routine returns (ASCII $01-$7F, or '\\n' for ENTER)`);
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Sign, zero, the undocumented bits 5 and 3, and parity, as the Z80 sets them for a logical op on `v`. */
|
|
55
|
+
function logicFlags(v) {
|
|
56
|
+
let parity = v;
|
|
57
|
+
parity ^= parity >> 4;
|
|
58
|
+
parity ^= parity >> 2;
|
|
59
|
+
parity ^= parity >> 1;
|
|
60
|
+
return (v & 0xa8) | (v === 0 ? 0x40 : 0) | ((parity & 1) === 0 ? 0x04 : 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** F after `CP 01H` with A = `a` (the last flag-setting instruction of the $002B path, at $3100). */
|
|
64
|
+
function cp01Flags(a) {
|
|
65
|
+
const r = (a - 1) & 0xff;
|
|
66
|
+
return (r & 0x80) | (r === 0 ? 0x40 : 0) | ((a & 0x0f) < 1 ? 0x10 : 0) | ((a ^ 1) & (a ^ r) & 0x80 ? 0x04 : 0) | 0x02 | (a < 1 ? 0x01 : 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The KDCB, the keyboard device control block at $4015: $002B loads DE with it and returns it there. */
|
|
70
|
+
const KDCB = 0x4015;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return from the ROM keyboard routine at `pc` ($002B or $0049) as the Model III ROM does with
|
|
74
|
+
* `code` in A, verified against the ROM listing (spec §6, "Keyboard-scan hook"): DE = $4015 (the
|
|
75
|
+
* KDCB), BC, HL, IX, IY and the alternate set unchanged, F as the routine's last flag-setting
|
|
76
|
+
* instruction leaves it (`CP 01H` at $3100 for $002B, `OR A` at $004C for $0049), and the return
|
|
77
|
+
* address popped. It also does what the CPU does at the end of any instruction: clears `afterPrefix`
|
|
78
|
+
* and counts a pending EI down. It costs what a RET costs, 10 T-states and one refresh-counter step; the
|
|
79
|
+
* routine's RAM side effects (its key-rollover buffer at $4036, the repeat counters) are not made.
|
|
80
|
+
* @param {import('../cpu/z80cpu.js').Z80CPU} cpu
|
|
81
|
+
* @param {number} pc
|
|
82
|
+
* @param {number} code
|
|
83
|
+
*/
|
|
84
|
+
export function romKeyReturn(cpu, pc, code) {
|
|
85
|
+
const r = cpu.registers;
|
|
86
|
+
cpu.afterPrefix = false;
|
|
87
|
+
r.A = code;
|
|
88
|
+
r.F = pc === 0x0049 ? logicFlags(code) : cp01Flags(code);
|
|
89
|
+
r.D = KDCB >> 8;
|
|
90
|
+
r.E = KDCB & 0xff;
|
|
91
|
+
const sp = r.SP;
|
|
92
|
+
r.PC = cpu.readMemory(sp) | (cpu.readMemory((sp + 1) & 0xffff) << 8);
|
|
93
|
+
r.SP = (sp + 2) & 0xffff;
|
|
94
|
+
cpu.incrementR();
|
|
95
|
+
// The end of any instruction (Z80CPU.executeInstruction): a pending EI counts down and takes effect.
|
|
96
|
+
if (cpu.pendingEI > 0) {
|
|
97
|
+
cpu.pendingEI--;
|
|
98
|
+
if (cpu.pendingEI === 0) {
|
|
99
|
+
cpu.IFF1 = true;
|
|
100
|
+
cpu.IFF2 = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cpu.lastOpCycles = 10;
|
|
104
|
+
cpu.cycles += 10;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {{ at: number, down: boolean, key: string, code: string }} KeyEvent
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
export class KeyQueue {
|
|
112
|
+
/** @type {import('./keyboard.js').KeyboardMatrix} */
|
|
113
|
+
#matrix;
|
|
114
|
+
/** @type {() => number} */
|
|
115
|
+
#now;
|
|
116
|
+
/** @type {KeyEvent[]} Matrix events in time order. */
|
|
117
|
+
#events = [];
|
|
118
|
+
/** @type {number} Index of the next undelivered matrix event. */
|
|
119
|
+
#next = 0;
|
|
120
|
+
/** @type {number[]} ROM codes waiting for the keyboard-scan hook. */
|
|
121
|
+
#feed = [];
|
|
122
|
+
#feedNext = 0;
|
|
123
|
+
/** @type {number} The T-state the queued typing ends at; new typing starts no earlier. */
|
|
124
|
+
#end = 0;
|
|
125
|
+
#seq = 0;
|
|
126
|
+
/** T-state of the next matrix event, Infinity when none: the Machine's cheap per-instruction check. */
|
|
127
|
+
nextAt = Infinity;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {import('./keyboard.js').KeyboardMatrix} matrix
|
|
131
|
+
* @param {() => number} now the current T-state
|
|
132
|
+
*/
|
|
133
|
+
constructor(matrix, now) {
|
|
134
|
+
this.#matrix = matrix;
|
|
135
|
+
this.#now = now;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Press a key now and hold it until `release`.
|
|
140
|
+
* @param {string} key a character ('A', '1', ' ') or a name: ENTER, CLEAR, BREAK, UP, DOWN, LEFT, RIGHT, SHIFT, SPACE
|
|
141
|
+
*/
|
|
142
|
+
press(key) {
|
|
143
|
+
this.#matrix.keyDown(this.#mapped(key, 'press'), `press:${key}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Release a key `press` holds.
|
|
148
|
+
* @param {string} key
|
|
149
|
+
*/
|
|
150
|
+
release(key) {
|
|
151
|
+
this.#matrix.keyUp(`press:${key}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Queue typing. In matrix mode each character is pressed for half of a 1/cps-second window and
|
|
156
|
+
* released for the other half, so a repeated letter registers twice; typing starts now, or when
|
|
157
|
+
* the typing already queued ends. In hook mode it is `feed`.
|
|
158
|
+
*
|
|
159
|
+
* Pick `cps` for the program. The default 10 cps holds each key 50 ms of emulated time, long
|
|
160
|
+
* enough for the ROM's keyboard driver, which debounces. A program that reads the keyboard
|
|
161
|
+
* slowly, or only once a frame or less often, can miss a short tap: Rear Guard needs ENTER held
|
|
162
|
+
* about half a second (`{ cps: 1 }`). A high `cps` shortens the hold and can drop keys, and
|
|
163
|
+
* typing at the ROM's line editor with no pause for the ENTER can lose characters. For
|
|
164
|
+
* `press` and `release`, use the same name for both: a mismatch leaves the key down.
|
|
165
|
+
*
|
|
166
|
+
* `str` is a string of characters, or an array whose every element is one key: a character or
|
|
167
|
+
* a key name (`['A', 'ENTER', 'UP']`), so named keys can be typed in sequence; in hook mode
|
|
168
|
+
* every element must be a character the ROM routine returns.
|
|
169
|
+
* @param {string | string[]} str '\n' is ENTER
|
|
170
|
+
* @param {{ cps?: number, mode?: 'matrix' | 'hook' }} [opts]
|
|
171
|
+
*/
|
|
172
|
+
type(str, { cps = 10, mode = 'matrix' } = {}) {
|
|
173
|
+
if (mode === 'hook') return this.feed(str);
|
|
174
|
+
if (mode !== 'matrix') throw new Error(`type: mode must be 'matrix' or 'hook', not ${String(mode)}`);
|
|
175
|
+
if (!(cps > 0) || !Number.isFinite(cps)) throw new Error(`type: cps must be a positive number, not ${String(cps)}`);
|
|
176
|
+
const period = Math.round(CPU_CLOCK_HZ / cps);
|
|
177
|
+
const hold = Math.round(period / 2);
|
|
178
|
+
const keys = Array.from(str, (ch) => this.#mapped(ch, 'type'));
|
|
179
|
+
this.#compact();
|
|
180
|
+
let t = Math.max(this.#now(), this.#end);
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
const code = `queue:${this.#seq++}`;
|
|
183
|
+
this.#events.push({ at: t, down: true, key, code }, { at: t + hold, down: false, key, code });
|
|
184
|
+
t += period;
|
|
185
|
+
}
|
|
186
|
+
this.#end = t;
|
|
187
|
+
this.nextAt = this.#events[this.#next]?.at ?? Infinity;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Queue characters for the ROM keyboard-scan hook: each is returned once by the next call to
|
|
192
|
+
* $002B or $0049. The matrix is not touched.
|
|
193
|
+
* All characters are checked first, so a bad one throws and queues nothing.
|
|
194
|
+
* @param {string | string[]} str '\n' is ENTER; an array holds one character an element
|
|
195
|
+
*/
|
|
196
|
+
feed(str) {
|
|
197
|
+
const codes = Array.from(str, romCode);
|
|
198
|
+
for (const code of codes) this.#feed.push(code);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Queued events not yet delivered: matrix presses and releases, and fed characters. */
|
|
202
|
+
get pending() {
|
|
203
|
+
return this.#events.length - this.#next + this.#feed.length - this.#feedNext;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** True when a fed character is waiting for the hook. */
|
|
207
|
+
get hasFeed() {
|
|
208
|
+
return this.#feedNext < this.#feed.length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The next fed character's ROM code, removed from the buffer. */
|
|
212
|
+
takeFeed() {
|
|
213
|
+
const code = this.#feed[this.#feedNext++];
|
|
214
|
+
if (this.#feedNext === this.#feed.length) {
|
|
215
|
+
this.#feed.length = 0;
|
|
216
|
+
this.#feedNext = 0;
|
|
217
|
+
}
|
|
218
|
+
return code;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Deliver every matrix event due at T-state `now`, in order.
|
|
223
|
+
* @param {number} now
|
|
224
|
+
*/
|
|
225
|
+
drain(now) {
|
|
226
|
+
const events = this.#events;
|
|
227
|
+
while (this.#next < events.length && events[this.#next].at <= now) {
|
|
228
|
+
const e = events[this.#next++];
|
|
229
|
+
if (e.down) this.#matrix.keyDown(e.key, e.code);
|
|
230
|
+
else this.#matrix.keyUp(e.code);
|
|
231
|
+
}
|
|
232
|
+
this.nextAt = this.#next < events.length ? events[this.#next].at : Infinity;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Drop everything queued, releasing any queued key that is down; keys held with `press` stay down. */
|
|
236
|
+
clear() {
|
|
237
|
+
for (let i = this.#next; i < this.#events.length; i++) {
|
|
238
|
+
if (!this.#events[i].down) this.#matrix.keyUp(this.#events[i].code);
|
|
239
|
+
}
|
|
240
|
+
this.#events = [];
|
|
241
|
+
this.#next = 0;
|
|
242
|
+
this.#feed.length = 0;
|
|
243
|
+
this.#feedNext = 0;
|
|
244
|
+
this.#end = 0;
|
|
245
|
+
this.nextAt = Infinity;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#compact() {
|
|
249
|
+
if (this.#next > 0) {
|
|
250
|
+
this.#events = this.#events.slice(this.#next);
|
|
251
|
+
this.#next = 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {string} key @param {string} who */
|
|
256
|
+
#mapped(key, who) {
|
|
257
|
+
const k = matrixKey(key);
|
|
258
|
+
if (!this.#matrix.knows(k)) throw new Error(`${who}: no matrix key for ${JSON.stringify(key)}`);
|
|
259
|
+
return k;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III Keyboard Matrix
|
|
3
|
+
*
|
|
4
|
+
* The keyboard is an 8x8 switch matrix memory-mapped at 0x3800-0x3BFF.
|
|
5
|
+
* Address bits 0-7 select rows; reading returns the OR of every selected
|
|
6
|
+
* row's key bits (bit set = key down). The ROM scans by reading
|
|
7
|
+
* 0x3801, 0x3802, 0x3804 ... 0x3880.
|
|
8
|
+
*
|
|
9
|
+
* Row layout:
|
|
10
|
+
* row 0: @ A B C D E F G row 4: 0 1 2 3 4 5 6 7
|
|
11
|
+
* row 1: H I J K L M N O row 5: 8 9 : ; , - . /
|
|
12
|
+
* row 2: P Q R S T U V W row 6: ENT CLR BRK ↑ ↓ ← → SPC
|
|
13
|
+
* row 3: X Y Z row 7: SHIFT (bit 0), right SHIFT (bit 1)
|
|
14
|
+
*
|
|
15
|
+
* Browser keys are mapped by the character they produce (e.key), with a
|
|
16
|
+
* synthetic SHIFT for characters that live on a shifted TRS-80 key (e.g.
|
|
17
|
+
* `"` is SHIFT+2). Each press is remembered by its physical key (e.code)
|
|
18
|
+
* so keyUp releases exactly the combo keyDown pressed.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SHIFT = { row: 7, bit: 0 };
|
|
22
|
+
|
|
23
|
+
function key(row, bit) {
|
|
24
|
+
return [{ row, bit }];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shifted(row, bit) {
|
|
28
|
+
return [{ row, bit }, SHIFT];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildKeyMap() {
|
|
32
|
+
const map = new Map();
|
|
33
|
+
|
|
34
|
+
// Letters: rows 0-3, @ at row 0 bit 0, then A-Z in order.
|
|
35
|
+
// Model III convention: unshifted letter keys type UPPERCASE, and
|
|
36
|
+
// SHIFT+letter types lowercase. Browser lowercase input maps to the
|
|
37
|
+
// bare key; browser uppercase (physically shifted) maps to SHIFT+key,
|
|
38
|
+
// matching the real machine exactly.
|
|
39
|
+
map.set("@", key(0, 0));
|
|
40
|
+
for (let i = 0; i < 26; i++) {
|
|
41
|
+
const letter = String.fromCharCode(0x41 + i); // 'A'...
|
|
42
|
+
const pos = i + 1; // A is row 0 bit 1
|
|
43
|
+
const row = Math.floor(pos / 8);
|
|
44
|
+
const bit = pos % 8;
|
|
45
|
+
map.set(letter.toLowerCase(), key(row, bit));
|
|
46
|
+
map.set(letter, shifted(row, bit));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Digits: 0-7 on row 4, 8-9 on row 5 bits 0-1
|
|
50
|
+
for (let d = 0; d <= 7; d++) {
|
|
51
|
+
map.set(String(d), key(4, d));
|
|
52
|
+
}
|
|
53
|
+
map.set("8", key(5, 0));
|
|
54
|
+
map.set("9", key(5, 1));
|
|
55
|
+
|
|
56
|
+
// Unshifted punctuation on row 5
|
|
57
|
+
map.set(":", key(5, 2));
|
|
58
|
+
map.set(";", key(5, 3));
|
|
59
|
+
map.set(",", key(5, 4));
|
|
60
|
+
map.set("-", key(5, 5));
|
|
61
|
+
map.set(".", key(5, 6));
|
|
62
|
+
map.set("/", key(5, 7));
|
|
63
|
+
|
|
64
|
+
// Shifted symbols per the Level II keyboard legends
|
|
65
|
+
map.set("!", shifted(4, 1));
|
|
66
|
+
map.set('"', shifted(4, 2));
|
|
67
|
+
map.set("#", shifted(4, 3));
|
|
68
|
+
map.set("$", shifted(4, 4));
|
|
69
|
+
map.set("%", shifted(4, 5));
|
|
70
|
+
map.set("&", shifted(4, 6));
|
|
71
|
+
map.set("'", shifted(4, 7));
|
|
72
|
+
map.set("(", shifted(5, 0));
|
|
73
|
+
map.set(")", shifted(5, 1));
|
|
74
|
+
map.set("*", shifted(5, 2));
|
|
75
|
+
map.set("+", shifted(5, 3));
|
|
76
|
+
map.set("<", shifted(5, 4));
|
|
77
|
+
map.set("=", shifted(5, 5));
|
|
78
|
+
map.set(">", shifted(5, 6));
|
|
79
|
+
map.set("?", shifted(5, 7));
|
|
80
|
+
|
|
81
|
+
// Control and navigation keys (row 6) and shift (row 7)
|
|
82
|
+
map.set("Enter", key(6, 0));
|
|
83
|
+
map.set("Clear", key(6, 1)); // dedicated CLEAR if the browser reports one
|
|
84
|
+
map.set("Home", key(6, 1)); // common stand-in for CLEAR
|
|
85
|
+
map.set("Escape", key(6, 2)); // BREAK
|
|
86
|
+
map.set("Pause", key(6, 2)); // BREAK on keyboards that have it
|
|
87
|
+
map.set("ArrowUp", key(6, 3));
|
|
88
|
+
map.set("ArrowDown", key(6, 4));
|
|
89
|
+
map.set("ArrowLeft", key(6, 5));
|
|
90
|
+
map.set("Backspace", key(6, 5)); // ← doubles as delete in Level II
|
|
91
|
+
map.set("ArrowRight", key(6, 6));
|
|
92
|
+
map.set(" ", key(6, 7));
|
|
93
|
+
map.set("Shift", [SHIFT]);
|
|
94
|
+
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const KEY_MAP = buildKeyMap();
|
|
99
|
+
|
|
100
|
+
export class KeyboardMatrix {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.rows = new Uint8Array(8);
|
|
103
|
+
// physical key id (e.code) -> combo currently held for it
|
|
104
|
+
this.pressed = new Map();
|
|
105
|
+
this._boundKeyDown = null;
|
|
106
|
+
this._boundKeyUp = null;
|
|
107
|
+
this._element = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Row-select read: OR of every row whose select bit is set. */
|
|
111
|
+
read(select) {
|
|
112
|
+
let value = 0;
|
|
113
|
+
let sel = select & 0xff;
|
|
114
|
+
for (let row = 0; row < 8; row++) {
|
|
115
|
+
if (sel & (1 << row)) {
|
|
116
|
+
value |= this.rows[row];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Press the combo for a browser key. Returns true if the key maps to
|
|
124
|
+
* the TRS-80 matrix (caller should preventDefault), false otherwise.
|
|
125
|
+
*/
|
|
126
|
+
keyDown(browserKey, physicalCode) {
|
|
127
|
+
const combo = KEY_MAP.get(browserKey);
|
|
128
|
+
if (!combo) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
this.pressed.set(physicalCode || browserKey, combo);
|
|
132
|
+
this._rebuild();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** True when `browserKey` maps to the TRS-80 matrix. */
|
|
137
|
+
knows(browserKey) {
|
|
138
|
+
return KEY_MAP.has(browserKey);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Release whatever combo the physical key pressed. */
|
|
142
|
+
keyUp(physicalCode) {
|
|
143
|
+
if (!this.pressed.delete(physicalCode)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
this._rebuild();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Directly press a matrix position (used by tests and the system). */
|
|
151
|
+
pressKey(row, bit) {
|
|
152
|
+
this.pressed.set(`matrix:${row}:${bit}`, key(row, bit));
|
|
153
|
+
this._rebuild();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
releaseKey(row, bit) {
|
|
157
|
+
this.pressed.delete(`matrix:${row}:${bit}`);
|
|
158
|
+
this._rebuild();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
reset() {
|
|
162
|
+
this.pressed.clear();
|
|
163
|
+
this.rows.fill(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The matrix is always derived from the full set of held combos, so
|
|
167
|
+
// overlapping synthetic shifts can't get out of sync on release.
|
|
168
|
+
_rebuild() {
|
|
169
|
+
this.rows.fill(0);
|
|
170
|
+
for (const combo of this.pressed.values()) {
|
|
171
|
+
for (const { row, bit } of combo) {
|
|
172
|
+
this.rows[row] |= 1 << bit;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III line printer (ports 0xF8-0xFB)
|
|
3
|
+
*
|
|
4
|
+
* The parallel printer interface answers status reads and takes data
|
|
5
|
+
* writes on port 0xF8; all four addresses 0xF8-0xFB decode to it.
|
|
6
|
+
* Status 0x30 is "selected, ready" — busy (bit 7) and out-of-paper
|
|
7
|
+
* (bit 6) clear, unit-select (bit 5) and the held-high line (bit 4)
|
|
8
|
+
* set — which is what the ROM's LPRINT driver polls for before every
|
|
9
|
+
* byte. This printer is never busy and never out of paper: bytes land
|
|
10
|
+
* on an in-memory printout the UI offers as a .txt download.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const STATUS_READY = 0x30;
|
|
14
|
+
|
|
15
|
+
export class Printer {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.bytes = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
status() {
|
|
21
|
+
return STATUS_READY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
write(byte) {
|
|
25
|
+
this.bytes.push(byte & 0xff);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get length() {
|
|
29
|
+
return this.bytes.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The printout as text — the TRS-80 ends lines with 0x0D. */
|
|
33
|
+
text() {
|
|
34
|
+
let out = "";
|
|
35
|
+
for (const b of this.bytes) {
|
|
36
|
+
out += b === 0x0d ? "\n" : String.fromCharCode(b);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear() {
|
|
42
|
+
this.bytes = [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model III's only sound source is the cassette output on port $FF. This device keeps every
|
|
3
|
+
* write to that port, raw and stamped with the CPU's T-state count, in a fixed ring of 65,536
|
|
4
|
+
* events (typed arrays, no allocation per write). The newest events are kept; `overflowed`
|
|
5
|
+
* counts the ones lost to wrap-around. It is not part of serializeState.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SIZE = 65536;
|
|
9
|
+
|
|
10
|
+
export class Sound {
|
|
11
|
+
#cycles = new Float64Array(SIZE);
|
|
12
|
+
#values = new Uint8Array(SIZE);
|
|
13
|
+
#head = 0; // next slot to write
|
|
14
|
+
#count = 0; // events held, at most SIZE
|
|
15
|
+
/** @type {number} Events lost to wrap-around since the last clear. */
|
|
16
|
+
overflowed = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Record one port $FF write. Called by the I/O system.
|
|
20
|
+
* @param {number} cycle T-states at the write.
|
|
21
|
+
* @param {number} value The raw byte written.
|
|
22
|
+
*/
|
|
23
|
+
record(cycle, value) {
|
|
24
|
+
this.#cycles[this.#head] = cycle;
|
|
25
|
+
this.#values[this.#head] = value;
|
|
26
|
+
this.#head = (this.#head + 1) & (SIZE - 1);
|
|
27
|
+
if (this.#count < SIZE) this.#count++;
|
|
28
|
+
else this.overflowed++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Every port $FF write after a cycle, oldest first.
|
|
33
|
+
* @param {number} [sinceCycle] Only events with `cycle > sinceCycle`; the default -1 returns everything, including a write at cycle 0.
|
|
34
|
+
* @returns {{ cycle: number, value: number }[]}
|
|
35
|
+
*/
|
|
36
|
+
events(sinceCycle = -1) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const start = (this.#head - this.#count + SIZE) & (SIZE - 1);
|
|
39
|
+
for (let k = 0; k < this.#count; k++) {
|
|
40
|
+
const i = (start + k) & (SIZE - 1);
|
|
41
|
+
const cycle = this.#cycles[i];
|
|
42
|
+
if (cycle > sinceCycle) out.push({ cycle, value: this.#values[i] });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Empty the ring and reset `overflowed`. */
|
|
48
|
+
clear() {
|
|
49
|
+
this.#head = 0;
|
|
50
|
+
this.#count = 0;
|
|
51
|
+
this.overflowed = 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model III video as a text readout: 16 lines over video RAM at $3C00-$3FFF, 64 columns, or
|
|
3
|
+
* 32 in 32-column mode (port $EC bit 2), where the hardware shows the even-address bytes
|
|
4
|
+
* double-wide. It reads the live video RAM; it draws nothing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fromBase64 } from '../debug/state.js';
|
|
8
|
+
|
|
9
|
+
export class Video {
|
|
10
|
+
#system;
|
|
11
|
+
|
|
12
|
+
/** @param {import('../machine/trs80-system.js').TRS80System} system */
|
|
13
|
+
constructor(system) {
|
|
14
|
+
this.#system = system;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @returns {Uint8Array} A live view of $3C00-$3FFF (1024 bytes). */
|
|
18
|
+
get raw() {
|
|
19
|
+
return this.#system.memory.videoRam;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @returns {64 | 32} */
|
|
23
|
+
get columns() {
|
|
24
|
+
return this.#system.columns32 ? 32 : 64;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The screen as 16 lines of text. Bytes below $20 show as +$40 (the Model III character
|
|
29
|
+
* generator folds them to $40-$5F), and graphics and special bytes ($80-$FF) show as '.'.
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
text() {
|
|
33
|
+
const ram = this.raw;
|
|
34
|
+
const step = this.#system.columns32 ? 2 : 1;
|
|
35
|
+
const cols = 64 / step;
|
|
36
|
+
const rows = [];
|
|
37
|
+
for (let row = 0; row < 16; row++) {
|
|
38
|
+
let line = '';
|
|
39
|
+
for (let col = 0; col < cols; col++) {
|
|
40
|
+
let byte = ram[row * 64 + col * step];
|
|
41
|
+
if (byte < 0x20) byte += 0x40;
|
|
42
|
+
line += byte >= 0x20 && byte < 0x80 ? String.fromCharCode(byte) : '.';
|
|
43
|
+
}
|
|
44
|
+
rows.push(line);
|
|
45
|
+
}
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The cells of video RAM that differ from a snapshot's (`m.debug.snapshot()`), row-major, as
|
|
51
|
+
* byte values over the 64-column RAM layout (32-column mode shows only even columns).
|
|
52
|
+
* @param {any} snap
|
|
53
|
+
* @returns {{ row: number, col: number, was: number, now: number }[]}
|
|
54
|
+
*/
|
|
55
|
+
diff(snap) {
|
|
56
|
+
const text = snap?.memory?.videoRam;
|
|
57
|
+
if (typeof text !== 'string') throw new Error('video.diff needs a snapshot from m.debug.snapshot()');
|
|
58
|
+
const was = fromBase64(text);
|
|
59
|
+
const now = this.raw;
|
|
60
|
+
const out = [];
|
|
61
|
+
for (let i = 0; i < now.length; i++) {
|
|
62
|
+
if (was[i] !== now[i]) out.push({ row: i >> 6, col: i & 63, was: was[i], now: now[i] });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fugloy/emu — headless, deterministic TRS-80 Model III 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 { TRS80System, CPU_CLOCK_HZ, RTC_HZ, RTC_INTERVAL_TSTATES } from './machine/trs80-system.js';
|
|
10
|
+
export { Z80CPU } from './cpu/z80cpu.js';
|
|
11
|
+
export { MemorySystem } from './machine/memory.js';
|
|
12
|
+
export { IOSystem, RTC_IRQ_BIT, CASSETTE_RISE_IRQ_BIT, CASSETTE_FALL_IRQ_BIT, NMI_INTRQ_BIT, MODE_32COL_BIT } from './machine/io.js';
|
|
13
|
+
export { KeyboardMatrix } from './devices/keyboard.js';
|
|
14
|
+
export { CassetteSystem } from './devices/cassette.js';
|
|
15
|
+
export { casToPulses, pulsesToBytes, ensureLeader } from './devices/cassette-tape.js';
|
|
16
|
+
export { FDC1793, parseWriteTrackStream } from './devices/fdc-wd1793.js';
|
|
17
|
+
export { DiskImage } from './devices/disk-image.js';
|
|
18
|
+
export { Printer } from './devices/printer.js';
|
|
19
|
+
export { parseCas, fastLoadBasic, fastLoadSystem } from './loaders/cas-format.js';
|
|
20
|
+
export { parseBas } from './loaders/bas.js';
|
|
21
|
+
export { parseCmd, fastLoadCmd } from './loaders/cmd-format.js';
|
|
22
|
+
export { STATE_VERSION, STATE_MACHINE, toBase64, fromBase64, serializeState, restoreState } from './debug/state.js';
|