@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.
@@ -0,0 +1,513 @@
1
+ /**
2
+ * TRS-80 Model III I/O System
3
+ *
4
+ * Non-disk machine port map:
5
+ * 0xE0-0xE3 read: interrupt latch, active-low (pending bit reads 0)
6
+ * write: interrupt mask (bit set = source enabled)
7
+ * 0xEC-0xEF read: acknowledge/clear the RTC interrupt
8
+ * write: mode register (bit 1 = cassette motor,
9
+ * bit 2 = 32-column mode, bit 3 = alt charset)
10
+ * 0xE4-0xE7 read: NMI status, active-low (bit 7 = FDC INTRQ)
11
+ * write: NMI mask (bit 7 = FDC INTRQ raises NMI)
12
+ * 0xF0-0xF3 WD1793 floppy controller; with no disk mounted anywhere it
13
+ * is absent and reads float to 0xFF
14
+ * 0xF4 drive select (write): bits 0-3 drive, bit 4 side, bit 6
15
+ * WAIT, bit 7 MFM
16
+ * 0xF8-0xFB line printer (read: status, write: data byte)
17
+ * 0xFF cassette port (write bits 0-1 = output level,
18
+ * read: bit 7 = 500-baud pulse latch, bit 0 = 1500-baud
19
+ * level, bits 5-1 = port 0xEC's latch, bit 6 high)
20
+ *
21
+ * The RTC heartbeat interrupt fires at 30 Hz; its latch bit is RTC_IRQ_BIT.
22
+ * The keyboard is memory-mapped at 0x3800-0x3BFF (see MemorySystem), not
23
+ * an I/O port.
24
+ *
25
+ * The Model I has only port 0xFF (cassette, sound, and the video mode; its
26
+ * own handler, initializeModel1CassettePort): its other devices are
27
+ * memory-mapped by the Expansion Interface at 0x37E0-0x37EF
28
+ * (src/machine/models.js), which MemorySystem hands to `readExpansion`,
29
+ * `peekExpansion`, and `writeExpansion` here, so every other port floats to
30
+ * 0xFF. With the Expansion Interface (`expansion`, the default) the Model I
31
+ * has the interrupt latch at 0x37E0 (`latch`, devices/interrupt-latch.js),
32
+ * the drive select at 0x37E1, the printer at 0x37E8, and the WD1771 at
33
+ * 0x37EC-0x37EF (devices/fdc-wd1771.js, its INTRQ on the latch's bit 6);
34
+ * without it `latch` is null and nothing interrupts. The controller object
35
+ * exists on both models, clocked at the model's CPU clock (a WD1793 on the
36
+ * Model III, a WD1771 on the Model I); the Model I does not reach it through
37
+ * ports, and without the Expansion Interface not at all.
38
+ */
39
+
40
+ import { CassetteSystem } from "../devices/cassette.js";
41
+ import { FDC1793 } from "../devices/fdc-wd1793.js";
42
+ import { FDC1771 } from "../devices/fdc-wd1771.js";
43
+ import { Printer } from "../devices/printer.js";
44
+ import { InterruptLatch } from "../devices/interrupt-latch.js";
45
+ import { MODEL_III } from "./models.js";
46
+ import { decodeModel1Select, decodeModel3Select } from "./drive-select.js";
47
+
48
+ export const RTC_IRQ_BIT = 0x04;
49
+ // Cassette-edge interrupt sources (from the ROM's ISR at 0x35C2: E0
50
+ // bit 0 dispatches to a handler expecting one input level, bit 1 the
51
+ // other; each reads the level back from port 0xFF bit 0). The 1500-baud
52
+ // reader masks in rises only (0xE0 write = 0x01) and counts full
53
+ // cycles between interrupts.
54
+ export const CASSETTE_RISE_IRQ_BIT = 0x01;
55
+ export const CASSETTE_FALL_IRQ_BIT = 0x02;
56
+
57
+ // NMI mask/status bits (port 0xE4)
58
+ export const NMI_INTRQ_BIT = 0x80;
59
+
60
+ // Mode register (port 0xEC write) bits
61
+ export const MODE_32COL_BIT = 0x04; // 32-character display mode
62
+
63
+ // The Model I's port 0xFF output latch (Z59), D0-D3: "D0 and D1 ... are used
64
+ // to output data that is recorded on tape during a CSAVE function. D2 ...
65
+ // controls the status of the Recorder's motor. D3 ... controls the status of
66
+ // MODESEL*" (TRS-80 Micro Computer Technical Reference Handbook, 2nd ed.,
67
+ // 1982, p. 45, "OUTSIG*"). On the Model I `modeRegister` holds this latch.
68
+ export const M1_MOTOR_BIT = 0x04;
69
+ export const M1_MODESEL_BIT = 0x08; // 32-character display mode
70
+
71
+ export { decodeModel1Select, decodeModel3Select } from "./drive-select.js";
72
+
73
+ export class IOSystem {
74
+ /**
75
+ * @param {{ model?: import('./models.js').Model, expansion?: boolean }} [opts] the model whose
76
+ * ports to install (the Model III by default), and on the Model I whether the Expansion
77
+ * Interface is fitted (by default it is)
78
+ */
79
+ constructor({ model = MODEL_III, expansion = true } = {}) {
80
+ this.model = model;
81
+ // The Expansion Interface's interrupt latch at 0x37E0: the one object the timer (bit 7) and
82
+ // the disk controller (bit 6, emu phase 6 Task 3) raise and clear their bits in.
83
+ /** @type {InterruptLatch | null} */
84
+ this.latch = model.id === 1 && expansion ? new InterruptLatch() : null;
85
+ this.cassette = new CassetteSystem();
86
+ // A Model I recording decodes in Model III T-states (cassette.js)
87
+ if (model.id !== 3) this.cassette.timeScale = MODEL_III.clockHz / model.clockHz;
88
+ this.fdc = model.id === 3 ? new FDC1793({ cpuHz: model.clockHz }) : new FDC1771({ cpuHz: model.clockHz, latch: this.latch });
89
+ this.printer = new Printer();
90
+ this.keyboardBuffer = []; // legacy internal buffer (not a Model III port)
91
+ this.portHandlers = new Map();
92
+
93
+ // Interrupt latch (pending sources) and mask (enabled sources)
94
+ this.intLatch = 0;
95
+ this.intMask = 0;
96
+
97
+ // NMI mask (port 0xE4 write): bit 7 = FDC INTRQ triggers NMI
98
+ this.nmiMask = 0;
99
+
100
+ // Mode register (port 0xEC write); on the Model I, the port 0xFF
101
+ // output latch, D0-D3 (M1_MOTOR_BIT, M1_MODESEL_BIT)
102
+ this.modeRegister = 0;
103
+
104
+ // Fired when a mode-register write changes the value (the display
105
+ // reads bit 2, so the renderer must repaint even though video RAM
106
+ // itself didn't change). Wired by TRS80System.
107
+ this.onModeWrite = null;
108
+
109
+ // Cassette output level (port 0xFF write, bits 0-1)
110
+ this.cassetteOut = 0;
111
+
112
+ // Cassette-out transitions for the sound driver: {t, level} stamped
113
+ // in CPU T-states. getCycles is wired by TRS80System; the driver
114
+ // drains the log every frame. Capped so an undrained log (sound off,
115
+ // headless run) cannot grow without bound.
116
+ this.getCycles = null;
117
+ this.soundLog = [];
118
+ this.SOUND_LOG_LIMIT = 100000;
119
+ // Raw port 0xFF write sink (cycles, value): the Machine's sound ring.
120
+ this.onSoundWrite = null;
121
+ // Called when the timeline restarts (state restore): the Machine clears its sound ring.
122
+ this.onSoundReset = null;
123
+ // The Model III WAIT (port F4 bit 6): called with a function that runs the controller to
124
+ // its next DRQ or INTRQ and returns the T-states that took, for the CPU to wait them; wired
125
+ // by TRS80System. Unwired, port F4 does not wait.
126
+ this.onWait = null;
127
+
128
+ if (model.id === 3) {
129
+ this.initializeModelIIIPorts();
130
+ this.initializeCassettePort();
131
+ } else {
132
+ this.initializeModel1CassettePort();
133
+ // The Model I interrupts through the Expansion Interface's latch alone, which has no mask
134
+ // (none without the interface); the Model III's pendingInterrupt below is left as it was.
135
+ const latch = this.latch;
136
+ this.pendingInterrupt = latch === null ? () => false : () => latch.bits !== 0;
137
+ }
138
+ }
139
+
140
+ initializeModelIIIPorts() {
141
+ // 0xE0-0xE3: interrupt latch (read, active-low) / mask (write)
142
+ for (let port = 0xe0; port <= 0xe3; port++) {
143
+ this.portHandlers.set(port, {
144
+ read: () => ~this.intLatch & 0xff,
145
+ write: (value) => {
146
+ this.intMask = value & 0xff;
147
+ },
148
+ });
149
+ }
150
+
151
+ // 0xEC-0xEF: RTC acknowledge (read) / mode register (write)
152
+ for (let port = 0xec; port <= 0xef; port++) {
153
+ this.portHandlers.set(port, {
154
+ read: () => {
155
+ this.intLatch &= ~RTC_IRQ_BIT;
156
+ return 0xff;
157
+ },
158
+ write: (value) => {
159
+ const changed = this.modeRegister !== (value & 0xff);
160
+ this.modeRegister = value & 0xff;
161
+ // Mode register bit 1 drives the cassette motor relay;
162
+ // CassetteSystem.control expects the motor state in bit 0.
163
+ this.cassette.control(
164
+ (value >> 1) & 0x01,
165
+ this.getCycles ? this.getCycles() : 0
166
+ );
167
+ if (changed && this.onModeWrite) {
168
+ this.onModeWrite(this.modeRegister);
169
+ }
170
+ },
171
+ });
172
+ }
173
+
174
+ // 0xE4-0xE7: disk NMI mask (write) / status (read, active-low).
175
+ // With no controller "present" (no disk mounted) the port floats.
176
+ // Bit 7 is the FDC's INTRQ, read live. MAME's trs80m3 driver also
177
+ // names bit 6 (motor timeout, its own NMI enable) and bit 5 (the reset
178
+ // button); neither is modeled, and none of this is yet checked against
179
+ // the Model III Technical Reference Manual (spec §5).
180
+ for (let port = 0xe4; port <= 0xe7; port++) {
181
+ this.portHandlers.set(port, {
182
+ read: () => {
183
+ if (!this.fdc.anyDiskMounted()) return 0xff;
184
+ let value = 0xff;
185
+ if (this.fdc.intrq) value &= ~NMI_INTRQ_BIT;
186
+ return value;
187
+ },
188
+ write: (value) => {
189
+ this.nmiMask = value & 0xff;
190
+ },
191
+ });
192
+ }
193
+
194
+ // 0xF0-0xF3: WD1793 FDC. The controller presents as absent (floating
195
+ // bus) until a disk is mounted — that float is what sends the boot
196
+ // ROM to cassette BASIC on a driveless machine.
197
+ const fdcPresent = () => this.fdc.anyDiskMounted();
198
+ this.portHandlers.set(0xf0, {
199
+ read: () => (fdcPresent() ? this.fdc.readStatus() : 0xff),
200
+ write: (v) => {
201
+ if (fdcPresent()) this.fdc.writeCommand(v);
202
+ },
203
+ });
204
+ this.portHandlers.set(0xf1, {
205
+ read: () => (fdcPresent() ? this.fdc.readTrackReg() : 0xff),
206
+ write: (v) => {
207
+ if (fdcPresent()) this.fdc.writeTrackReg(v);
208
+ },
209
+ });
210
+ this.portHandlers.set(0xf2, {
211
+ read: () => (fdcPresent() ? this.fdc.readSectorReg() : 0xff),
212
+ write: (v) => {
213
+ if (fdcPresent()) this.fdc.writeSectorReg(v);
214
+ },
215
+ });
216
+ this.portHandlers.set(0xf3, {
217
+ read: () => (fdcPresent() ? this.fdc.readData() : 0xff),
218
+ write: (v) => {
219
+ if (fdcPresent()) this.fdc.writeData(v);
220
+ },
221
+ });
222
+
223
+ // 0xF4: drive select / side / density latch. Bit 6 asserts WAIT
224
+ // until the controller raises DRQ or INTRQ (MAME's trs80m3: only when
225
+ // both are low); the ROM's boot read and the DOSes' INI loops rely on
226
+ // it to take each byte as it arrives.
227
+ this.portHandlers.set(0xf4, {
228
+ read: () => 0xff,
229
+ write: (v) => {
230
+ if (!fdcPresent()) return;
231
+ const sel = decodeModel3Select(v);
232
+ this.fdc.select(sel.drive, sel.side, sel.mfm);
233
+ if (sel.wait && this.onWait) this.onWait(() => this.fdc.waitForDrq());
234
+ },
235
+ });
236
+
237
+ // 0xF8-0xFB: line printer. Status reads ready; data writes land on
238
+ // the printout. Note io.reset() leaves the printout alone — paper
239
+ // already printed survives a reboot on the real machine too.
240
+ for (let port = 0xf8; port <= 0xfb; port++) {
241
+ this.portHandlers.set(port, {
242
+ read: () => this.printer.status(),
243
+ write: (v) => this.printer.write(v),
244
+ });
245
+ }
246
+ }
247
+
248
+ /** Port 0xFF on the Model III. */
249
+ initializeCassettePort() {
250
+ // 0xFF: cassette data port. Reads return the input latch in bit 7
251
+ // (set by tape transitions while the motor runs); ANY write clears
252
+ // the latch — that's the Model III's latch circuit, and it's how
253
+ // the ROM's read routines re-arm between pulses. Output level
254
+ // changes are the machine's only sound source, so they're logged
255
+ // for the audio driver, and they double as the recording the deck
256
+ // can decode back into a .cas.
257
+ this.portHandlers.set(0xff, {
258
+ read: () => {
259
+ const cycles = this.getCycles ? this.getCycles() : 0;
260
+ // Two input bits, straight from the ROM's own readers:
261
+ // bit 0 — the tape signal's live LEVEL (the 1500-baud ISR
262
+ // does IN A,(0xFF) / AND 0x01 / CP E per edge);
263
+ // bit 7 — a rising-edge PULSE LATCH (the 500-baud reader
264
+ // polls it with RLA and resets it by OUT (0xFF),A).
265
+ // Bits 5-1 read port 0xEC's latch back (DISWAIT, ENEXTIO, ENALTSET,
266
+ // MODSEL, CASMOTORON), and bit 6 is undefined: Model III Technical
267
+ // Reference Manual p. 22 (CASIN*). Undriven, bit 6 floats high, as
268
+ // an absent port's bits do; trs80gp (and trsjs) read it as 1 too.
269
+ // Reading also acks the edge interrupt sources: the ISR reads
270
+ // 0xE0 then 0xFF and nothing else.
271
+ this.intLatch &= ~(CASSETTE_RISE_IRQ_BIT | CASSETTE_FALL_IRQ_BIT);
272
+ const level = this.cassette.tapeLevel(cycles);
273
+ return (this.cassette.pulseLatch ? 0x80 : 0x00) | 0x40 | (this.modeRegister & 0x3e) | (level ? 0x01 : 0x00);
274
+ },
275
+ write: (value) => {
276
+ const cycles = this.getCycles ? this.getCycles() : 0;
277
+ const level = value & 0x03;
278
+ if (this.onSoundWrite) this.onSoundWrite(cycles, value);
279
+ if (level !== this.cassetteOut) {
280
+ this.cassetteOut = level;
281
+ if (this.getCycles && this.soundLog.length < this.SOUND_LOG_LIMIT) {
282
+ this.soundLog.push({ t: cycles, level });
283
+ }
284
+ this.cassette.recordTransition(cycles, level);
285
+ }
286
+ this.cassette.clearPulseLatch(cycles);
287
+ },
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Port 0xFF on the Model I. The port is the Z59 output latch and one input
293
+ * flip-flop (Technical Reference Handbook, 2nd ed., p. 45, "OUTSIG*",
294
+ * and pp. 50-51, "INSIG*"):
295
+ * write D0-D1 cassette output level (the only sound source; logged for
296
+ * the sound driver and m.sound, and recorded while the
297
+ * motor runs, as on the Model III)
298
+ * write D2 the cassette motor relay
299
+ * write D3 MODESEL: 32 characters a line when set
300
+ * any write resets the cassette flip-flop ("set by cassette data and
301
+ * reset by OUTSIG*", p. 50)
302
+ * read D7 the cassette flip-flop: the Level II ROM's bit reader
303
+ * (MDL1LEV2.Z80, CTBIT at $0241) polls it with RLA and resets
304
+ * it through CTSTAT ($021E), an OUT of the shadow copy CAST
305
+ * The other read bits are MAME's (trs80 driver, port_ff_r: D6 is MODESEL
306
+ * inverted, D0-D5 read 1); the handbook says only that INSIG* enables the
307
+ * flip-flop's buffer, so they are unverified. The deck sets the flip-flop
308
+ * on a rising edge of the tape signal (cassette.js); MAME takes the
309
+ * falling edge, which for the 500-baud pulses moves nothing a reader sees.
310
+ */
311
+ initializeModel1CassettePort() {
312
+ this.portHandlers.set(0xff, {
313
+ read: () => {
314
+ const cycles = this.getCycles ? this.getCycles() : 0;
315
+ this.cassette.tapeLevel(cycles); // play the tape up to now
316
+ return (
317
+ (this.cassette.pulseLatch ? 0x80 : 0x00) |
318
+ (this.modeRegister & M1_MODESEL_BIT ? 0x00 : 0x40) |
319
+ 0x3f
320
+ );
321
+ },
322
+ write: (value) => {
323
+ const cycles = this.getCycles ? this.getCycles() : 0;
324
+ if (this.onSoundWrite) this.onSoundWrite(cycles, value);
325
+ const motor = (value & M1_MOTOR_BIT) !== 0;
326
+ if (motor !== this.cassette.motorOn) {
327
+ this.cassette.control(motor ? 1 : 0, cycles);
328
+ }
329
+ const level = value & 0x03;
330
+ if (level !== this.cassetteOut) {
331
+ this.cassetteOut = level;
332
+ if (this.getCycles && this.soundLog.length < this.SOUND_LOG_LIMIT) {
333
+ this.soundLog.push({ t: cycles, level });
334
+ }
335
+ this.cassette.recordTransition(cycles, level);
336
+ }
337
+ const latch = value & 0x0f;
338
+ const changed = (latch ^ this.modeRegister) & M1_MODESEL_BIT;
339
+ this.modeRegister = latch;
340
+ if (changed && this.onModeWrite) this.onModeWrite(this.modeRegister);
341
+ this.cassette.clearPulseLatch(cycles);
342
+ },
343
+ });
344
+ }
345
+
346
+ /**
347
+ * A CPU read of the Model I's Expansion Interface addresses 0x37E0-0x37EF
348
+ * (MemorySystem calls it only on a Model I with the Expansion Interface):
349
+ * 0x37E0-0x37E3 the interrupt latch, clearing its clock bit
350
+ * (interrupt-latch.js; the mirrors are MAME's, unverified)
351
+ * 0x37E4-0x37E7 0xFF (the cassette select at 0x37E4 is write-only, and a
352
+ * second deck is not modelled)
353
+ * 0x37E8-0x37EB the printer's status: the Level II ROM's PRTAVL at $05D1
354
+ * reads 0x37E8 and takes (status AND F0H) = 30H as ready
355
+ * (MDL1LEV2.Z80); the mirrors are MAME's (unverified)
356
+ * 0x37EC-0x37EF the WD1771: status, track, sector, data (CONSTANTS.Z80,
357
+ * "Memory Mapped Hardware, Model 1"); a status read clears
358
+ * INTRQ and with it the latch's disk bit
359
+ * With no disk mounted in any drive the controller is taken as absent, as on
360
+ * the Model III, and 0x37EC-0x37EF read 0xFF: the ROM's test at $0696-$069C
361
+ * (status + 1 < 2, that is $FF or $00: no disk controller) then sends it to
362
+ * Level II BASIC instead of a boot that would wait for a disk forever.
363
+ * @param {number} address
364
+ */
365
+ readExpansion(address) {
366
+ if (address <= 0x37e3) return this.latch.read();
367
+ if (address >= 0x37ec) {
368
+ if (!this.fdc.anyDiskMounted()) return 0xff;
369
+ switch (address) {
370
+ case 0x37ec:
371
+ return this.fdc.readStatus();
372
+ case 0x37ed:
373
+ return this.fdc.readTrackReg();
374
+ case 0x37ee:
375
+ return this.fdc.readSectorReg();
376
+ default:
377
+ return this.fdc.readData();
378
+ }
379
+ }
380
+ if (address >= 0x37e8) return this.printer.status();
381
+ return 0xff;
382
+ }
383
+
384
+ /**
385
+ * What `readExpansion` would return, with no side effect (the latch keeps
386
+ * its clock bit, the controller its INTRQ and DRQ).
387
+ * @param {number} address
388
+ */
389
+ peekExpansion(address) {
390
+ if (address <= 0x37e3) return this.latch.peek();
391
+ if (address >= 0x37ec) {
392
+ if (!this.fdc.anyDiskMounted()) return 0xff;
393
+ if (address === 0x37ec) return this.fdc.peekStatus();
394
+ if (address === 0x37ed) return this.fdc.trackReg;
395
+ if (address === 0x37ee) return this.fdc.sectorReg;
396
+ return this.fdc.dataReg;
397
+ }
398
+ if (address >= 0x37e8) return this.printer.status();
399
+ return 0xff;
400
+ }
401
+
402
+ /**
403
+ * A CPU write to 0x37E0-0x37EF:
404
+ * 0x37E0-0x37E3 the drive select (decodeModel1Select; written at 0x37E1,
405
+ * the mirrors are MAME's, unverified). The latch is the
406
+ * Expansion Interface's, not the controller's, so it takes
407
+ * a write with no disk mounted too, and a disk mounted
408
+ * later finds the drive the program last selected
409
+ * 0x37E8-0x37EB a byte to the printer (the ROM's `LD (37E8H),A` at $05BB)
410
+ * 0x37EC-0x37EF the WD1771: command, track, sector, data
411
+ * The controller's registers ignore writes while no disk is mounted
412
+ * (absent, as above).
413
+ * @param {number} address
414
+ * @param {number} value
415
+ */
416
+ writeExpansion(address, value) {
417
+ if (address <= 0x37e3) {
418
+ this.fdc.select(decodeModel1Select(value).drive, 0, false);
419
+ return;
420
+ }
421
+ if (address >= 0x37ec) {
422
+ if (!this.fdc.anyDiskMounted()) return;
423
+ switch (address) {
424
+ case 0x37ec:
425
+ this.fdc.writeCommand(value);
426
+ return;
427
+ case 0x37ed:
428
+ this.fdc.writeTrackReg(value);
429
+ return;
430
+ case 0x37ee:
431
+ this.fdc.writeSectorReg(value);
432
+ return;
433
+ default:
434
+ this.fdc.writeData(value);
435
+ }
436
+ return;
437
+ }
438
+ if (address >= 0x37e8) this.printer.write(value);
439
+ }
440
+
441
+ readPort(port) {
442
+ port &= 0xff;
443
+ const handler = this.portHandlers.get(port);
444
+ return handler?.read ? handler.read() : 0xff;
445
+ }
446
+
447
+ writePort(port, value) {
448
+ port &= 0xff;
449
+ value &= 0xff;
450
+ const handler = this.portHandlers.get(port);
451
+ if (handler?.write) {
452
+ handler.write(value);
453
+ }
454
+ }
455
+
456
+ /** Raise the 30 Hz real-time clock interrupt line. */
457
+ raiseRTC() {
458
+ this.intLatch |= RTC_IRQ_BIT;
459
+ }
460
+
461
+ /** True when an enabled interrupt source is pending. */
462
+ pendingInterrupt() {
463
+ // The cassette edge is a live source: advance the pulse deck to
464
+ // "now" so an edge that just played raises the latch bit without
465
+ // waiting for a port access. Guarded, so machines without a tape
466
+ // playing pay one property check.
467
+ if (this.cassette.pulseTape && this.cassette.motorOn && this.getCycles) {
468
+ const edges = this.cassette.consumeEdges(this.getCycles());
469
+ if (edges.rise) this.intLatch |= CASSETTE_RISE_IRQ_BIT;
470
+ if (edges.fall) this.intLatch |= CASSETTE_FALL_IRQ_BIT;
471
+ }
472
+ return (this.intLatch & this.intMask) !== 0;
473
+ }
474
+
475
+ /** True when the FDC's INTRQ should raise an NMI. */
476
+ nmiPending() {
477
+ return this.fdc.intrq && (this.nmiMask & NMI_INTRQ_BIT) !== 0;
478
+ }
479
+
480
+ /** Hand the accumulated cassette-out transitions to the sound driver. */
481
+ drainSound() {
482
+ if (this.soundLog.length === 0) return this.soundLog;
483
+ const log = this.soundLog;
484
+ this.soundLog = [];
485
+ return log;
486
+ }
487
+
488
+ /** Clear interrupt state (system reset). Mounted disks stay mounted. */
489
+ reset() {
490
+ this.latch?.reset();
491
+ this.intLatch = 0;
492
+ this.intMask = 0;
493
+ this.nmiMask = 0;
494
+ this.modeRegister = 0;
495
+ this.cassetteOut = 0;
496
+ this.soundLog = [];
497
+ this.fdc.reset();
498
+ }
499
+
500
+ readKeyboard() {
501
+ return this.keyboardBuffer.length > 0 ? this.keyboardBuffer.shift() : 0x00;
502
+ }
503
+
504
+ addKey(keyCode) {
505
+ if (this.keyboardBuffer.length < 256) {
506
+ this.keyboardBuffer.push(keyCode & 0xff);
507
+ }
508
+ }
509
+
510
+ clearKeyboardBuffer() {
511
+ this.keyboardBuffer = [];
512
+ }
513
+ }