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