ac6502 1.9.3 → 1.11.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.
Files changed (77) hide show
  1. package/dist/components/CPU.d.ts +4 -0
  2. package/dist/components/CPU.js +87 -30
  3. package/dist/components/CPU.js.map +1 -1
  4. package/dist/components/IO/ACIA.d.ts +13 -21
  5. package/dist/components/IO/ACIA.js +53 -151
  6. package/dist/components/IO/ACIA.js.map +1 -1
  7. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.d.ts +5 -10
  8. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js +43 -266
  9. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js.map +1 -1
  10. package/dist/components/IO/Empty.d.ts +1 -3
  11. package/dist/components/IO/Empty.js +1 -5
  12. package/dist/components/IO/Empty.js.map +1 -1
  13. package/dist/components/IO/RAMBank.d.ts +3 -4
  14. package/dist/components/IO/RAMBank.js +4 -13
  15. package/dist/components/IO/RAMBank.js.map +1 -1
  16. package/dist/components/IO/RTC.d.ts +2 -3
  17. package/dist/components/IO/RTC.js +17 -7
  18. package/dist/components/IO/RTC.js.map +1 -1
  19. package/dist/components/IO/Sound.d.ts +1 -3
  20. package/dist/components/IO/Sound.js +13 -23
  21. package/dist/components/IO/Sound.js.map +1 -1
  22. package/dist/components/IO/Storage.d.ts +1 -3
  23. package/dist/components/IO/Storage.js +1 -3
  24. package/dist/components/IO/Storage.js.map +1 -1
  25. package/dist/components/IO/VIA.d.ts +1 -3
  26. package/dist/components/IO/VIA.js +6 -7
  27. package/dist/components/IO/VIA.js.map +1 -1
  28. package/dist/components/IO/Video.d.ts +1 -3
  29. package/dist/components/IO/Video.js +3 -5
  30. package/dist/components/IO/Video.js.map +1 -1
  31. package/dist/components/IO.d.ts +1 -3
  32. package/dist/components/Machine.d.ts +1 -2
  33. package/dist/components/Machine.js +21 -74
  34. package/dist/components/Machine.js.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/tests/IO/ACIA.test.js +57 -108
  38. package/dist/tests/IO/ACIA.test.js.map +1 -1
  39. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js +334 -574
  40. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js.map +1 -1
  41. package/dist/tests/IO/Empty.test.js +2 -14
  42. package/dist/tests/IO/Empty.test.js.map +1 -1
  43. package/dist/tests/IO/RAMBank.test.js +5 -12
  44. package/dist/tests/IO/RAMBank.test.js.map +1 -1
  45. package/dist/tests/IO/RTC.test.js +7 -16
  46. package/dist/tests/IO/RTC.test.js.map +1 -1
  47. package/dist/tests/IO/Sound.test.js +6 -8
  48. package/dist/tests/IO/Sound.test.js.map +1 -1
  49. package/dist/tests/IO/Storage.test.js +0 -6
  50. package/dist/tests/IO/Storage.test.js.map +1 -1
  51. package/dist/tests/IO/VIA.test.js +6 -10
  52. package/dist/tests/IO/VIA.test.js.map +1 -1
  53. package/dist/tests/IO/Video.test.js +7 -7
  54. package/dist/tests/IO/Video.test.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/CPU.ts +94 -31
  57. package/src/components/IO/ACIA.ts +57 -176
  58. package/src/components/IO/Attachments/KeyboardEncoderAttachment.ts +45 -217
  59. package/src/components/IO/Empty.ts +1 -4
  60. package/src/components/IO/RAMBank.ts +4 -15
  61. package/src/components/IO/RTC.ts +18 -7
  62. package/src/components/IO/Sound.ts +14 -27
  63. package/src/components/IO/Storage.ts +2 -5
  64. package/src/components/IO/VIA.ts +6 -8
  65. package/src/components/IO/Video.ts +5 -7
  66. package/src/components/IO.ts +1 -4
  67. package/src/components/Machine.ts +22 -90
  68. package/src/index.ts +1 -1
  69. package/src/tests/IO/ACIA.test.ts +60 -122
  70. package/src/tests/IO/Attachments/KeyboardEncoderAttachment.test.ts +342 -676
  71. package/src/tests/IO/Empty.test.ts +2 -17
  72. package/src/tests/IO/RAMBank.test.ts +5 -14
  73. package/src/tests/IO/RTC.test.ts +7 -20
  74. package/src/tests/IO/Sound.test.ts +6 -8
  75. package/src/tests/IO/Storage.test.ts +0 -7
  76. package/src/tests/IO/VIA.test.ts +6 -12
  77. package/src/tests/IO/Video.test.ts +7 -8
@@ -2,35 +2,35 @@ import { AttachmentBase } from './Attachment'
2
2
 
3
3
  /**
4
4
  * USB HID Keycode to ASCII mapping table
5
- * Maps USB HID usage IDs to ASCII characters
5
+ * Maps USB HID usage IDs to uppercase ASCII characters
6
6
  */
7
7
  const USB_HID_TO_ASCII: { [key: number]: number } = {
8
- 0x04: 0x61, // a
9
- 0x05: 0x62, // b
10
- 0x06: 0x63, // c
11
- 0x07: 0x64, // d
12
- 0x08: 0x65, // e
13
- 0x09: 0x66, // f
14
- 0x0A: 0x67, // g
15
- 0x0B: 0x68, // h
16
- 0x0C: 0x69, // i
17
- 0x0D: 0x6A, // j
18
- 0x0E: 0x6B, // k
19
- 0x0F: 0x6C, // l
20
- 0x10: 0x6D, // m
21
- 0x11: 0x6E, // n
22
- 0x12: 0x6F, // o
23
- 0x13: 0x70, // p
24
- 0x14: 0x71, // q
25
- 0x15: 0x72, // r
26
- 0x16: 0x73, // s
27
- 0x17: 0x74, // t
28
- 0x18: 0x75, // u
29
- 0x19: 0x76, // v
30
- 0x1A: 0x77, // w
31
- 0x1B: 0x78, // x
32
- 0x1C: 0x79, // y
33
- 0x1D: 0x7A, // z
8
+ 0x04: 0x41, // A
9
+ 0x05: 0x42, // B
10
+ 0x06: 0x43, // C
11
+ 0x07: 0x44, // D
12
+ 0x08: 0x45, // E
13
+ 0x09: 0x46, // F
14
+ 0x0A: 0x47, // G
15
+ 0x0B: 0x48, // H
16
+ 0x0C: 0x49, // I
17
+ 0x0D: 0x4A, // J
18
+ 0x0E: 0x4B, // K
19
+ 0x0F: 0x4C, // L
20
+ 0x10: 0x4D, // M
21
+ 0x11: 0x4E, // N
22
+ 0x12: 0x4F, // O
23
+ 0x13: 0x50, // P
24
+ 0x14: 0x51, // Q
25
+ 0x15: 0x52, // R
26
+ 0x16: 0x53, // S
27
+ 0x17: 0x54, // T
28
+ 0x18: 0x55, // U
29
+ 0x19: 0x56, // V
30
+ 0x1A: 0x57, // W
31
+ 0x1B: 0x58, // X
32
+ 0x1C: 0x59, // Y
33
+ 0x1D: 0x5A, // Z
34
34
  0x1E: 0x31, // 1
35
35
  0x1F: 0x32, // 2
36
36
  0x20: 0x33, // 3
@@ -68,27 +68,25 @@ const USB_HID_TO_ASCII: { [key: number]: number } = {
68
68
  /**
69
69
  * KeyboardEncoderAttachment - Emulates a keyboard encoder that provides ASCII-encoded
70
70
  * key data on both GPIO ports A and B.
71
- *
71
+ *
72
72
  * This attachment uses the VIA control lines to signal data availability:
73
73
  * - CA2 LOW enables Port A
74
74
  * - CB2 LOW enables Port B
75
75
  * - CA1 interrupt signals data ready on Port A
76
76
  * - CB1 interrupt signals data ready on Port B
77
- *
78
- * The encoder supports extensive modifier key combinations:
79
- * - MENU key: 0x80 (alone), 0x90 (with Alt)
80
- * - Function keys F1-F15: 0x81-0x8F (alone), 0x91-0x9F (with Alt)
81
- * - Ctrl combinations: Control codes 0x00-0x1F
82
- * - Alt+Shift: Extended character set 0xA0-0xFF
83
- * - Alt: Extended character set 0xE0-0xFF
84
- * - Shift: Uppercase letters and shifted symbols
77
+ *
78
+ * Letters are always output as uppercase ASCII (0x41-0x5A).
79
+ * Supported modifier combinations:
80
+ * - Ctrl+letter: Control codes 0x01-0x1A
81
+ * - Ctrl+special: Ctrl+2=NUL, Ctrl+6=RS, Ctrl+-=US, Ctrl+[=ESC, Ctrl+\=FS, Ctrl+]=GS
82
+ * - Shift+number/symbol: Standard US keyboard shifted symbols
85
83
  */
86
84
  export class KeyboardEncoderAttachment extends AttachmentBase {
87
85
  // Selects which port(s) receive data and fire interrupts:
88
86
  // 'A' = PS/2 encoder on Port A (CA1 IRQ only)
89
87
  // 'B' = Matrix encoder on Port B (CB1 IRQ only)
90
88
  // 'both' = both ports active (default)
91
- activePort: 'A' | 'B' | 'both' = 'both'
89
+ activePort: 'A' | 'B' | 'both' = 'B'
92
90
 
93
91
  // Port A state
94
92
  private asciiDataA: number = 0x00
@@ -105,9 +103,6 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
105
103
  // Modifier key states
106
104
  private shiftPressed: boolean = false
107
105
  private ctrlPressed: boolean = false
108
- private altPressed: boolean = false
109
- private menuPressed: boolean = false
110
- private capsLockActive: boolean = false
111
106
 
112
107
  // Control line states
113
108
  private stateCA1: boolean = false
@@ -133,9 +128,6 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
133
128
  this.enabledB = false
134
129
  this.shiftPressed = false
135
130
  this.ctrlPressed = false
136
- this.altPressed = false
137
- this.menuPressed = false
138
- this.capsLockActive = false
139
131
  }
140
132
 
141
133
  readPortA(ddrA: number, orA: number): number {
@@ -196,34 +188,6 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
196
188
  * Map a USB HID keycode to ASCII with modifier keys applied
197
189
  */
198
190
  private mapKeyWithModifiers(usbHidKeycode: number): number {
199
- // Handle MENU key (USB HID 0xE3 Left GUI, 0xE7 Right GUI)
200
- if (usbHidKeycode === 0xE3 || usbHidKeycode === 0xE7) {
201
- if (this.altPressed) {
202
- return 0x90 // Alt+MENU
203
- }
204
- return 0x80 // MENU alone
205
- }
206
-
207
- // Handle function keys F1-F15 (USB HID 0x3A-0x45 for F1-F12, 0x68-0x6A for F13-F15)
208
- // F1-F15 without Alt → 0x81-0x8F
209
- // Alt+F1-F15 → 0x91-0x9F
210
- if (usbHidKeycode >= 0x3A && usbHidKeycode <= 0x45) {
211
- // F1-F12 (0x3A-0x45)
212
- const fKeyOffset = usbHidKeycode - 0x3A // 0-11 for F1-F12
213
- if (this.altPressed) {
214
- return 0x91 + fKeyOffset // Alt+F1-F12 → 0x91-0x9C
215
- }
216
- return 0x81 + fKeyOffset // F1-F12 → 0x81-0x8C
217
- }
218
- if (usbHidKeycode >= 0x68 && usbHidKeycode <= 0x6A) {
219
- // F13-F15 (0x68-0x6A)
220
- const fKeyOffset = usbHidKeycode - 0x68 + 12 // 12-14 for F13-F15
221
- if (this.altPressed) {
222
- return 0x91 + fKeyOffset // Alt+F13-F15 → 0x9D-0x9F
223
- }
224
- return 0x81 + fKeyOffset // F13-F15 → 0x8D-0x8F
225
- }
226
-
227
191
  // Get base ASCII character from USB HID keycode
228
192
  const baseChar = USB_HID_TO_ASCII[usbHidKeycode] || 0x00
229
193
 
@@ -233,144 +197,25 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
233
197
  }
234
198
 
235
199
  // Handle Ctrl combinations - control codes
236
- if (this.ctrlPressed && !this.altPressed) {
200
+ if (this.ctrlPressed) {
237
201
  // Ctrl with letters produces control codes 0x01-0x1A
238
- if (baseChar >= 0x61 && baseChar <= 0x7A) { // a-z
239
- return baseChar - 0x61 + 0x01
240
- }
241
202
  if (baseChar >= 0x41 && baseChar <= 0x5A) { // A-Z
242
203
  return baseChar - 0x41 + 0x01
243
204
  }
244
205
 
245
206
  // Ctrl with other special keys
246
207
  switch (baseChar) {
247
- case 0x32: case 0x40: return 0x00 // Ctrl+2 or Ctrl+@ = NUL
248
- case 0x36: case 0x5E: return 0x1E // Ctrl+6 or Ctrl+^ = RS (UP arrow position)
249
- case 0x2D: case 0x5F: return 0x1F // Ctrl+- or Ctrl+_ = US (DOWN arrow position)
250
- case 0x5B: case 0x7B: return 0x1B // Ctrl+[ or Ctrl+{ = ESC
251
- case 0x5C: case 0x7C: return 0x1C // Ctrl+\ or Ctrl+| = FS (LEFT arrow position)
252
- case 0x5D: case 0x7D: return 0x1D // Ctrl+] or Ctrl+} = GS (RIGHT arrow position)
253
- }
254
- }
255
-
256
- // Handle Alt+Shift combinations - extended character set
257
- if (this.altPressed && this.shiftPressed) {
258
- switch (baseChar) {
259
- case 0x31: return 0xA1 // '1' -> ¡
260
- case 0x27: return 0xA2 // '\'' -> ¢
261
- case 0x33: return 0xA3 // '3' -> £
262
- case 0x34: return 0xA4 // '4' -> ¤
263
- case 0x35: return 0xA5 // '5' -> ¥
264
- case 0x37: return 0xA6 // '7' -> ¦
265
- case 0x39: return 0xA8 // '9' -> ¨
266
- case 0x30: return 0xA9 // '0' -> ©
267
- case 0x38: return 0xAA // '8' -> ª
268
- case 0x3D: return 0xAB // '=' -> «
269
- case 0x3B: return 0xBA // ';' -> º
270
- case 0x2C: return 0xBC // ',' -> ¼
271
- case 0x2E: return 0xBE // '.' -> ¾
272
- case 0x2F: return 0xBF // '/' -> ¿
273
- case 0x32: return 0xC0 // '2' -> À
274
- case 0x61: case 0x41: return 0xC1 // 'a'/'A' -> Á
275
- case 0x62: case 0x42: return 0xC2 // 'b'/'B' -> Â
276
- case 0x63: case 0x43: return 0xC3 // 'c'/'C' -> Ã
277
- case 0x64: case 0x44: return 0xC4 // 'd'/'D' -> Ä
278
- case 0x65: case 0x45: return 0xC5 // 'e'/'E' -> Å
279
- case 0x66: case 0x46: return 0xC6 // 'f'/'F' -> Æ
280
- case 0x67: case 0x47: return 0xC7 // 'g'/'G' -> Ç
281
- case 0x68: case 0x48: return 0xC8 // 'h'/'H' -> È
282
- case 0x69: case 0x49: return 0xC9 // 'i'/'I' -> É
283
- case 0x6A: case 0x4A: return 0xCA // 'j'/'J' -> Ê
284
- case 0x6B: case 0x4B: return 0xCB // 'k'/'K' -> Ë
285
- case 0x6C: case 0x4C: return 0xCC // 'l'/'L' -> Ì
286
- case 0x6D: case 0x4D: return 0xCD // 'm'/'M' -> Í
287
- case 0x6E: case 0x4E: return 0xCE // 'n'/'N' -> Î
288
- case 0x6F: case 0x4F: return 0xCF // 'o'/'O' -> Ï
289
- case 0x70: case 0x50: return 0xD0 // 'p'/'P' -> Ð
290
- case 0x71: case 0x51: return 0xD1 // 'q'/'Q' -> Ñ
291
- case 0x72: case 0x52: return 0xD2 // 'r'/'R' -> Ò
292
- case 0x73: case 0x53: return 0xD3 // 's'/'S' -> Ó
293
- case 0x74: case 0x54: return 0xD4 // 't'/'T' -> Ô
294
- case 0x75: case 0x55: return 0xD5 // 'u'/'U' -> Õ
295
- case 0x76: case 0x56: return 0xD6 // 'v'/'V' -> Ö
296
- case 0x77: case 0x57: return 0xD7 // 'w'/'W' -> ×
297
- case 0x78: case 0x58: return 0xD8 // 'x'/'X' -> Ø
298
- case 0x79: case 0x59: return 0xD9 // 'y'/'Y' -> Ù
299
- case 0x7A: case 0x5A: return 0xDA // 'z'/'Z' -> Ú
300
- case 0x5B: return 0xFB // '[' -> û
301
- case 0x5C: return 0xFC // '\\' -> ü
302
- case 0x5D: return 0xFD // ']' -> ý
303
- case 0x60: return 0xFE // '`' -> þ
304
- case 0x36: return 0xDE // '6' -> Þ
305
- case 0x2D: return 0xDF // '-' -> ß
306
- }
307
- }
308
-
309
- // Handle Alt combinations (without shift)
310
- if (this.altPressed && !this.shiftPressed) {
311
- switch (baseChar) {
312
- case 0x20: return 0xA0 // Space -> nbsp
313
- case 0x27: return 0xA7 // '\'' -> §
314
- case 0x2C: return 0xAC // ',' -> ¬
315
- case 0x2D: return 0xAD // '-' -> soft hyphen
316
- case 0x2E: return 0xAE // '.' -> ®
317
- case 0x2F: return 0xAF // '/' -> ¯
318
- case 0x30: return 0xB0 // '0' -> °
319
- case 0x31: return 0xB1 // '1' -> ±
320
- case 0x32: return 0xB2 // '2' -> ²
321
- case 0x33: return 0xB3 // '3' -> ³
322
- case 0x34: return 0xB4 // '4' -> ´
323
- case 0x35: return 0xB5 // '5' -> µ
324
- case 0x36: return 0xB6 // '6' -> ¶
325
- case 0x37: return 0xB7 // '7' -> ·
326
- case 0x38: return 0xB8 // '8' -> ¸
327
- case 0x39: return 0xB9 // '9' -> ¹
328
- case 0x3B: return 0xBB // ';' -> »
329
- case 0x3D: return 0xBD // '=' -> ½
330
- case 0x5B: return 0xDB // '[' -> Û
331
- case 0x5C: return 0xDC // '\\' -> Ü
332
- case 0x5D: return 0xDD // ']' -> Ý
333
- case 0x60: return 0xE0 // '`' -> à
334
- case 0x61: case 0x41: return 0xE1 // 'a'/'A' -> á
335
- case 0x62: case 0x42: return 0xE2 // 'b'/'B' -> â
336
- case 0x63: case 0x43: return 0xE3 // 'c'/'C' -> ã
337
- case 0x64: case 0x44: return 0xE4 // 'd'/'D' -> ä
338
- case 0x65: case 0x45: return 0xE5 // 'e'/'E' -> å
339
- case 0x66: case 0x46: return 0xE6 // 'f'/'F' -> æ
340
- case 0x67: case 0x47: return 0xE7 // 'g'/'G' -> ç
341
- case 0x68: case 0x48: return 0xE8 // 'h'/'H' -> è
342
- case 0x69: case 0x49: return 0xE9 // 'i'/'I' -> é
343
- case 0x6A: case 0x4A: return 0xEA // 'j'/'J' -> ê
344
- case 0x6B: case 0x4B: return 0xEB // 'k'/'K' -> ë
345
- case 0x6C: case 0x4C: return 0xEC // 'l'/'L' -> ì
346
- case 0x6D: case 0x4D: return 0xED // 'm'/'M' -> í
347
- case 0x6E: case 0x4E: return 0xEE // 'n'/'N' -> î
348
- case 0x6F: case 0x4F: return 0xEF // 'o'/'O' -> ï
349
- case 0x70: case 0x50: return 0xF0 // 'p'/'P' -> ð
350
- case 0x71: case 0x51: return 0xF1 // 'q'/'Q' -> ñ
351
- case 0x72: case 0x52: return 0xF2 // 'r'/'R' -> ò
352
- case 0x73: case 0x53: return 0xF3 // 's'/'S' -> ó
353
- case 0x74: case 0x54: return 0xF4 // 't'/'T' -> ô
354
- case 0x75: case 0x55: return 0xF5 // 'u'/'U' -> õ
355
- case 0x76: case 0x56: return 0xF6 // 'v'/'V' -> ö
356
- case 0x77: case 0x57: return 0xF7 // 'w'/'W' -> ÷
357
- case 0x78: case 0x58: return 0xF8 // 'x'/'X' -> ø
358
- case 0x79: case 0x59: return 0xF9 // 'y'/'Y' -> ù
359
- case 0x7A: case 0x5A: return 0xFA // 'z'/'Z' -> ú
360
- case 0x7F: return 0xFF // DEL -> ÿ
208
+ case 0x32: return 0x00 // Ctrl+2 = NUL
209
+ case 0x36: return 0x1E // Ctrl+6 = RS
210
+ case 0x2D: return 0x1F // Ctrl+- = US
211
+ case 0x5B: return 0x1B // Ctrl+[ = ESC
212
+ case 0x5C: return 0x1C // Ctrl+\ = FS
213
+ case 0x5D: return 0x1D // Ctrl+] = GS
361
214
  }
362
215
  }
363
216
 
364
- // Handle Shift combinations - uppercase and shifted symbols
365
- // Caps Lock XORs with Shift for letters (they cancel each other out)
366
- const effectiveUppercase = this.capsLockActive !== this.shiftPressed
367
- if (effectiveUppercase && !this.ctrlPressed && !this.altPressed) {
368
- if (baseChar >= 0x61 && baseChar <= 0x7A) { // a-z
369
- return baseChar - 0x61 + 0x41 // A-Z
370
- }
371
- }
372
- if (this.shiftPressed && !this.ctrlPressed && !this.altPressed) {
373
- // Shifted symbols (Caps Lock does not affect symbols)
217
+ // Handle Shift combinations - shifted symbols only (letters are always uppercase)
218
+ if (this.shiftPressed && !this.ctrlPressed) {
374
219
  switch (baseChar) {
375
220
  case 0x31: return 0x21 // '1' -> '!'
376
221
  case 0x32: return 0x40 // '2' -> '@'
@@ -413,27 +258,10 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
413
258
  this.ctrlPressed = pressed
414
259
  return // Don't generate output for modifier keys alone
415
260
 
416
- case 0x39: // Caps Lock - toggle on press, ignore release
417
- if (pressed) {
418
- this.capsLockActive = !this.capsLockActive
419
- }
420
- return
421
-
422
261
  case 0xE1: // Left Shift
423
262
  case 0xE5: // Right Shift
424
263
  this.shiftPressed = pressed
425
264
  return
426
-
427
- case 0xE2: // Left Alt
428
- case 0xE6: // Right Alt
429
- this.altPressed = pressed
430
- return
431
-
432
- case 0xE3: // Left GUI (MENU)
433
- case 0xE7: // Right GUI (MENU)
434
- this.menuPressed = pressed
435
- // MENU key generates output, so don't return - fall through
436
- break
437
265
  }
438
266
 
439
267
  // Only process key presses, not releases (encoder only reports keypress events)
@@ -445,7 +273,7 @@ export class KeyboardEncoderAttachment extends AttachmentBase {
445
273
  const mappedValue = this.mapKeyWithModifiers(usbHidKeycode)
446
274
 
447
275
  // Ignore keys with no mapping (0x00 unless it's a valid control code like Ctrl+2 = NUL)
448
- // Valid 0x00: Ctrl+2, Ctrl+@
276
+ // Valid 0x00: Ctrl+2
449
277
  if (mappedValue === 0x00 && !this.ctrlPressed) {
450
278
  return
451
279
  }
@@ -2,15 +2,12 @@ import { IO } from '../IO'
2
2
 
3
3
  export class Empty implements IO {
4
4
 
5
- raiseIRQ = () => {}
6
- raiseNMI = () => {}
7
-
8
5
  read(address: number): number {
9
6
  return 0
10
7
  }
11
8
 
12
9
  write(address: number, data: number): void {}
13
- tick(frequency: number): void {}
10
+ tick(frequency: number): number { return 0 }
14
11
  reset(coldStart: boolean): void {}
15
12
 
16
13
  }
@@ -20,40 +20,29 @@ export class RAMBank implements IO {
20
20
  data: number[] = [...Array(RAMBank.TOTAL_SIZE)].fill(0x00)
21
21
  currentBank: number = 0
22
22
 
23
- raiseIRQ = () => {}
24
- raiseNMI = () => {}
25
-
26
23
  /**
27
- * Read from RAM or bank control register
24
+ * Read from RAM - all addresses read from the data array
28
25
  */
29
26
  read(address: number): number {
30
- // Reading from bank control register returns current bank number
31
- if (address === RAMBank.BANK_CONTROL_REGISTER) {
32
- return this.currentBank
33
- }
34
-
35
- // Calculate actual address in RAM: bank * bank_size + offset and return data
36
27
  return this.data[this.currentBank * RAMBank.BANK_SIZE + address]
37
28
  }
38
29
 
39
30
  /**
40
31
  * Write to RAM or bank control register
32
+ * Writing to $3FF sets the bank AND writes through to the new bank's data
41
33
  */
42
34
  write(address: number, data: number): void {
43
- // Writing to bank control register switches banks
44
35
  if (address === RAMBank.BANK_CONTROL_REGISTER) {
45
- this.currentBank = data & 0xFF // Ensure 0-255 range
46
- return
36
+ this.currentBank = data & 0xFF
47
37
  }
48
38
 
49
- // Calculate actual address in RAM: bank * bank_size + offset and store data
50
39
  this.data[this.currentBank * RAMBank.BANK_SIZE + address] = data & 0xFF
51
40
  }
52
41
 
53
42
  /**
54
43
  * Tick - no timing behavior for RAM
55
44
  */
56
- tick(frequency: number): void {}
45
+ tick(frequency: number): number { return 0 }
57
46
 
58
47
  /**
59
48
  * Reset the RAM card
@@ -26,8 +26,6 @@ import { IO } from '../IO'
26
26
  * 0x13: RAM Data (Extended RAM Data at address pointed to by 0x10)
27
27
  */
28
28
  export class RTC implements IO {
29
- raiseIRQ = () => {}
30
- raiseNMI = () => {}
31
29
 
32
30
  // RTC Registers (user-visible)
33
31
  private userSeconds: number = 0 // 0x00
@@ -61,6 +59,7 @@ export class RTC implements IO {
61
59
  private watchdog2: number = 0 // 0x0D (0.1 Second and Second)
62
60
  private watchdogCounterCentis: number = 0
63
61
  private watchdogCycleCounter: number = 0
62
+ private nmiPending: boolean = false
64
63
 
65
64
  // Control registers
66
65
  private controlA: number = 0 // 0x0E
@@ -261,7 +260,6 @@ export class RTC implements IO {
261
260
  if ((this.controlA & flagMask) === 0) return
262
261
  if ((this.controlB & enableMask) === 0) return
263
262
  this.controlA |= 0x01 // Set IRQF flag (bit 0)
264
- this.raiseIRQ()
265
263
  }
266
264
 
267
265
  private setKickstartFlag(): void {
@@ -304,7 +302,7 @@ export class RTC implements IO {
304
302
  } else {
305
303
  // WDS=1 steers watchdog to reset; emulate by clearing WDE
306
304
  this.controlB &= ~0x02
307
- this.raiseNMI()
305
+ this.nmiPending = true
308
306
  }
309
307
  }
310
308
 
@@ -449,7 +447,7 @@ export class RTC implements IO {
449
447
 
450
448
 
451
449
 
452
- tick(frequency: number, cycles: number = 1): void {
450
+ tick(frequency: number): number {
453
451
  this.cpuFrequency = frequency > 0 ? frequency : 1000000
454
452
 
455
453
  const teEnabled = (this.controlB & 0x80) !== 0
@@ -457,12 +455,17 @@ export class RTC implements IO {
457
455
  if ((this.monthControl & 0x80) !== 0) {
458
456
  // EOSC=1: oscillator disabled (DS1511Y+ active-low enable)
459
457
  this.stepWatchdog()
460
- return
458
+ let status = (this.controlA & 0x01) ? 0x80 : 0
459
+ if (this.nmiPending) {
460
+ status |= 0x40
461
+ this.nmiPending = false
462
+ }
463
+ return status
461
464
  }
462
465
 
463
466
  // Advance the clock by counting CPU cycles.
464
467
  // One emulated second = cpuFrequency cycles.
465
- this.cycleAccumulator += cycles
468
+ this.cycleAccumulator++
466
469
  while (this.cycleAccumulator >= this.cpuFrequency) {
467
470
  this.cycleAccumulator -= this.cpuFrequency
468
471
  this.incrementTime()
@@ -482,6 +485,14 @@ export class RTC implements IO {
482
485
  }
483
486
 
484
487
  this.stepWatchdog()
488
+
489
+ // Return interrupt status: bit 7 = IRQ, bit 6 = NMI
490
+ let status = (this.controlA & 0x01) ? 0x80 : 0
491
+ if (this.nmiPending) {
492
+ status |= 0x40
493
+ this.nmiPending = false
494
+ }
495
+ return status
485
496
  }
486
497
 
487
498
  reset(coldStart: boolean): void {
@@ -40,9 +40,6 @@ export const SID_CLOCK_PAL = 985248
40
40
  /** Number of SID registers */
41
41
  const NUM_REGISTERS = 29
42
42
 
43
- /** Cycles per tick from Machine.ts ioTickInterval */
44
- const CYCLES_PER_TICK = 128
45
-
46
43
  // Register offsets within each voice (relative to voice base)
47
44
  const REG_FREQ_LO = 0
48
45
  const REG_FREQ_HI = 1
@@ -162,9 +159,6 @@ export class SIDVoice {
162
159
 
163
160
  export class Sound implements IO {
164
161
 
165
- raiseIRQ = () => {}
166
- raiseNMI = () => {}
167
-
168
162
  /** Callback to push audio samples to the host emulator */
169
163
  pushSamples?: (samples: Float32Array) => void
170
164
 
@@ -202,7 +196,7 @@ export class Sound implements IO {
202
196
  sidClock: number = SID_CLOCK_NTSC
203
197
 
204
198
  /** Internal sample buffer for pushing to host */
205
- private sampleBuffer: Float32Array = new Float32Array(4096)
199
+ private sampleBuffer: Float32Array = new Float32Array(128)
206
200
  private sampleBufferIndex: number = 0
207
201
 
208
202
  // ================================================================
@@ -247,35 +241,28 @@ export class Sound implements IO {
247
241
  }
248
242
  }
249
243
 
250
- tick(frequency: number): void {
244
+ tick(frequency: number): number {
251
245
  // SID clock runs at the CPU clock rate
252
246
  this.sidClock = frequency
253
247
 
254
- // Each tick represents CYCLES_PER_TICK SID clock cycles
255
- const cycles = CYCLES_PER_TICK
256
-
257
- for (let c = 0; c < cycles; c++) {
258
- this.clockOneCycle()
248
+ // Each tick represents 1 SID clock cycle
249
+ this.clockOneCycle()
259
250
 
260
- // Sample rate conversion: accumulate and downsample
261
- this.cycleAccumulator += this.sampleRate
262
- if (this.cycleAccumulator >= this.sidClock) {
263
- this.cycleAccumulator -= this.sidClock
251
+ // Sample rate conversion: accumulate and downsample
252
+ this.cycleAccumulator += this.sampleRate
253
+ if (this.cycleAccumulator >= this.sidClock) {
254
+ this.cycleAccumulator -= this.sidClock
264
255
 
265
- const sample = this.generateSample()
266
- this.sampleBuffer[this.sampleBufferIndex++] = sample
256
+ const sample = this.generateSample()
257
+ this.sampleBuffer[this.sampleBufferIndex++] = sample
267
258
 
268
- // Buffer full - push to host
269
- if (this.sampleBufferIndex >= this.sampleBuffer.length) {
270
- this.flushSampleBuffer()
271
- }
259
+ // Buffer full - push to host
260
+ if (this.sampleBufferIndex >= this.sampleBuffer.length) {
261
+ this.flushSampleBuffer()
272
262
  }
273
263
  }
274
264
 
275
- // Flush remaining samples
276
- if (this.sampleBufferIndex > 0) {
277
- this.flushSampleBuffer()
278
- }
265
+ return 0
279
266
  }
280
267
 
281
268
  reset(coldStart: boolean): void {
@@ -65,9 +65,6 @@ export class Storage implements IO {
65
65
  private isIdentifying: boolean = false
66
66
  private isTransferring: boolean = false
67
67
 
68
- raiseIRQ = () => {}
69
- raiseNMI = () => {}
70
-
71
68
  constructor() {
72
69
  // Initialize storage and identity buffers
73
70
  this.storage = new Uint8Array(Storage.STORAGE_SIZE)
@@ -129,8 +126,8 @@ export class Storage implements IO {
129
126
  }
130
127
  }
131
128
 
132
- tick(frequency: number): void {
133
- // No timing behavior needed for this implementation
129
+ tick(frequency: number): number {
130
+ return 0
134
131
  }
135
132
 
136
133
  reset(coldStart: boolean): void {
@@ -78,9 +78,6 @@ export class VIA implements IO {
78
78
  private portA_attachmentCount: number = 0
79
79
  private portB_attachmentCount: number = 0
80
80
 
81
- raiseIRQ = () => {}
82
- raiseNMI = () => {}
83
-
84
81
  constructor() {
85
82
  this.reset(true)
86
83
  }
@@ -333,12 +330,12 @@ export class VIA implements IO {
333
330
  }
334
331
  }
335
332
 
336
- tick(frequency: number, cycles: number = 1): void {
333
+ tick(frequency: number): number {
337
334
  this.tickCounter++
338
335
 
339
336
  // Update Timer 1
340
337
  if (this.T1_running && this.regT1C > 0) {
341
- this.regT1C -= cycles
338
+ this.regT1C--
342
339
  if (this.regT1C <= 0) {
343
340
  this.setIRQFlag(VIA.IRQ_T1)
344
341
 
@@ -359,7 +356,7 @@ export class VIA implements IO {
359
356
 
360
357
  // Update Timer 2
361
358
  if (this.T2_running && this.regT2C > 0) {
362
- this.regT2C -= cycles
359
+ this.regT2C--
363
360
  if (this.regT2C <= 0) {
364
361
  this.setIRQFlag(VIA.IRQ_T2)
365
362
  this.regT2C = 0
@@ -401,10 +398,11 @@ export class VIA implements IO {
401
398
  }
402
399
  }
403
400
 
404
- // Raise IRQ if any enabled interrupt is active
401
+ // Return IRQ status if any enabled interrupt is active
405
402
  if (this.regIFR & this.regIER & 0x7F) {
406
- this.raiseIRQ()
403
+ return 0x80
407
404
  }
405
+ return 0
408
406
  }
409
407
 
410
408
  private updateIRQ(): void {