ac6502 1.0.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/components/CPU.js +1170 -0
  4. package/dist/components/CPU.js.map +1 -0
  5. package/dist/components/Cart.js +23 -0
  6. package/dist/components/Cart.js.map +1 -0
  7. package/dist/components/IO/Empty.js +19 -0
  8. package/dist/components/IO/Empty.js.map +1 -0
  9. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
  10. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
  11. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
  12. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
  13. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
  14. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
  15. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
  16. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
  17. package/dist/components/IO/GPIOCard.js +597 -0
  18. package/dist/components/IO/GPIOCard.js.map +1 -0
  19. package/dist/components/IO/InputBoard.js +19 -0
  20. package/dist/components/IO/InputBoard.js.map +1 -0
  21. package/dist/components/IO/LCDCard.js +19 -0
  22. package/dist/components/IO/LCDCard.js.map +1 -0
  23. package/dist/components/IO/RAMCard.js +63 -0
  24. package/dist/components/IO/RAMCard.js.map +1 -0
  25. package/dist/components/IO/RTCCard.js +483 -0
  26. package/dist/components/IO/RTCCard.js.map +1 -0
  27. package/dist/components/IO/SerialCard.js +282 -0
  28. package/dist/components/IO/SerialCard.js.map +1 -0
  29. package/dist/components/IO/SoundCard.js +620 -0
  30. package/dist/components/IO/SoundCard.js.map +1 -0
  31. package/dist/components/IO/StorageCard.js +428 -0
  32. package/dist/components/IO/StorageCard.js.map +1 -0
  33. package/dist/components/IO/VGACard.js +9 -0
  34. package/dist/components/IO/VGACard.js.map +1 -0
  35. package/dist/components/IO/VideoCard.js +623 -0
  36. package/dist/components/IO/VideoCard.js.map +1 -0
  37. package/dist/components/IO.js +3 -0
  38. package/dist/components/IO.js.map +1 -0
  39. package/dist/components/Machine.js +310 -0
  40. package/dist/components/Machine.js.map +1 -0
  41. package/dist/components/RAM.js +24 -0
  42. package/dist/components/RAM.js.map +1 -0
  43. package/dist/components/ROM.js +23 -0
  44. package/dist/components/ROM.js.map +1 -0
  45. package/dist/index.js +441 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tests/CPU.test.js +1626 -0
  48. package/dist/tests/CPU.test.js.map +1 -0
  49. package/dist/tests/Cart.test.js +119 -0
  50. package/dist/tests/Cart.test.js.map +1 -0
  51. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
  52. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
  53. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
  54. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
  55. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
  56. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
  57. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
  58. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
  59. package/dist/tests/IO/GPIOCard.test.js +503 -0
  60. package/dist/tests/IO/GPIOCard.test.js.map +1 -0
  61. package/dist/tests/IO/RAMCard.test.js +229 -0
  62. package/dist/tests/IO/RAMCard.test.js.map +1 -0
  63. package/dist/tests/IO/RTCCard.test.js +177 -0
  64. package/dist/tests/IO/RTCCard.test.js.map +1 -0
  65. package/dist/tests/IO/SerialCard.test.js +423 -0
  66. package/dist/tests/IO/SerialCard.test.js.map +1 -0
  67. package/dist/tests/IO/SoundCard.test.js +528 -0
  68. package/dist/tests/IO/SoundCard.test.js.map +1 -0
  69. package/dist/tests/IO/StorageCard.test.js +647 -0
  70. package/dist/tests/IO/StorageCard.test.js.map +1 -0
  71. package/dist/tests/IO/VideoCard.test.js +549 -0
  72. package/dist/tests/IO/VideoCard.test.js.map +1 -0
  73. package/dist/tests/Machine.test.js +383 -0
  74. package/dist/tests/Machine.test.js.map +1 -0
  75. package/dist/tests/RAM.test.js +160 -0
  76. package/dist/tests/RAM.test.js.map +1 -0
  77. package/dist/tests/ROM.test.js +123 -0
  78. package/dist/tests/ROM.test.js.map +1 -0
  79. package/jest.config.cjs +9 -0
  80. package/package.json +43 -0
  81. package/src/components/CPU.ts +1371 -0
  82. package/src/components/Cart.ts +20 -0
  83. package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
  84. package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
  85. package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
  86. package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
  87. package/src/components/IO/GPIOCard.ts +677 -0
  88. package/src/components/IO/RAMCard.ts +68 -0
  89. package/src/components/IO/RTCCard.ts +518 -0
  90. package/src/components/IO/SerialCard.ts +335 -0
  91. package/src/components/IO/SoundCard.ts +711 -0
  92. package/src/components/IO/StorageCard.ts +473 -0
  93. package/src/components/IO/VideoCard.ts +730 -0
  94. package/src/components/IO.ts +11 -0
  95. package/src/components/Machine.ts +364 -0
  96. package/src/components/RAM.ts +23 -0
  97. package/src/components/ROM.ts +19 -0
  98. package/src/index.ts +474 -0
  99. package/src/tests/CPU.test.ts +2045 -0
  100. package/src/tests/Cart.test.ts +149 -0
  101. package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
  102. package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
  103. package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
  104. package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
  105. package/src/tests/IO/GPIOCard.test.ts +644 -0
  106. package/src/tests/IO/RAMCard.test.ts +284 -0
  107. package/src/tests/IO/RTCCard.test.ts +222 -0
  108. package/src/tests/IO/SerialCard.test.ts +530 -0
  109. package/src/tests/IO/SoundCard.test.ts +659 -0
  110. package/src/tests/IO/StorageCard.test.ts +787 -0
  111. package/src/tests/IO/VideoCard.test.ts +668 -0
  112. package/src/tests/Machine.test.ts +437 -0
  113. package/src/tests/RAM.test.ts +196 -0
  114. package/src/tests/ROM.test.ts +154 -0
  115. package/tsconfig.json +12 -0
@@ -0,0 +1,730 @@
1
+ import { IO } from '../IO'
2
+
3
+ /**
4
+ * TMS9918 Video Display Processor Emulation
5
+ *
6
+ * Port mapping (address bit 0):
7
+ * Even address (bit 0 = 0): VRAM data read/write
8
+ * Odd address (bit 0 = 1): Register/address write / status read
9
+ *
10
+ * Display modes:
11
+ * Graphics I - 32x24 tiles, 8x8 patterns, 1-of-8 color groups
12
+ * Graphics II - 32x24 tiles, 8x8 patterns, per-row color
13
+ * Text - 40x24 tiles, 6x8 patterns, no sprites
14
+ * Multicolor - 32x24 blocks, 4x4 colored cells
15
+ *
16
+ * Output: 256x192 active area centered in a 320x240 RGBA buffer
17
+ *
18
+ * Reference: vrEmuTms9918 by Troy Schrapel
19
+ * https://github.com/visrealm/vrEmuTms9918
20
+ */
21
+
22
+ // Display modes
23
+ export enum TmsMode {
24
+ GRAPHICS_I = 0,
25
+ GRAPHICS_II = 1,
26
+ TEXT = 2,
27
+ MULTICOLOR = 3,
28
+ }
29
+
30
+ // TMS9918 Color indices
31
+ export enum TmsColor {
32
+ TRANSPARENT = 0,
33
+ BLACK = 1,
34
+ MED_GREEN = 2,
35
+ LT_GREEN = 3,
36
+ DK_BLUE = 4,
37
+ LT_BLUE = 5,
38
+ DK_RED = 6,
39
+ CYAN = 7,
40
+ MED_RED = 8,
41
+ LT_RED = 9,
42
+ DK_YELLOW = 10,
43
+ LT_YELLOW = 11,
44
+ DK_GREEN = 12,
45
+ MAGENTA = 13,
46
+ GREY = 14,
47
+ WHITE = 15,
48
+ }
49
+
50
+ // TMS9918 Palette – RGBA bytes (transparent rendered as opaque black)
51
+ const TMS_PALETTE: ReadonlyArray<readonly [number, number, number, number]> = [
52
+ [0x00, 0x00, 0x00, 0xFF], // 0 Transparent (opaque black on display)
53
+ [0x00, 0x00, 0x00, 0xFF], // 1 Black
54
+ [0x21, 0xC9, 0x42, 0xFF], // 2 Medium Green
55
+ [0x5E, 0xDC, 0x78, 0xFF], // 3 Light Green
56
+ [0x54, 0x55, 0xED, 0xFF], // 4 Dark Blue
57
+ [0x7D, 0x75, 0xFC, 0xFF], // 5 Light Blue
58
+ [0xD3, 0x52, 0x4D, 0xFF], // 6 Dark Red
59
+ [0x43, 0xEB, 0xF6, 0xFF], // 7 Cyan
60
+ [0xFD, 0x55, 0x54, 0xFF], // 8 Medium Red
61
+ [0xFF, 0x79, 0x78, 0xFF], // 9 Light Red
62
+ [0xD3, 0xC1, 0x53, 0xFF], // 10 Dark Yellow
63
+ [0xE5, 0xCE, 0x80, 0xFF], // 11 Light Yellow
64
+ [0x21, 0xB0, 0x3C, 0xFF], // 12 Dark Green
65
+ [0xC9, 0x5B, 0xBA, 0xFF], // 13 Magenta
66
+ [0xCC, 0xCC, 0xCC, 0xFF], // 14 Grey
67
+ [0xFF, 0xFF, 0xFF, 0xFF], // 15 White
68
+ ]
69
+
70
+ // VRAM
71
+ const VRAM_SIZE = 1 << 14 // 16KB
72
+ const VRAM_MASK = VRAM_SIZE - 1 // 0x3FFF
73
+
74
+ // Active display resolution
75
+ const TMS_PIXELS_X = 256
76
+ const TMS_PIXELS_Y = 192
77
+
78
+ // Output buffer resolution
79
+ const DISPLAY_WIDTH = 320
80
+ const DISPLAY_HEIGHT = 240
81
+
82
+ // Tile / character layout
83
+ const GRAPHICS_NUM_COLS = 32
84
+ const GRAPHICS_CHAR_WIDTH = 8
85
+ const TEXT_NUM_COLS = 40
86
+ const TEXT_CHAR_WIDTH = 6
87
+ const TEXT_PADDING_PX = 8
88
+
89
+ // Pattern table
90
+ const PATTERN_BYTES = 8
91
+ const GFXI_COLOR_GROUP_SIZE = 8
92
+
93
+ // Sprites
94
+ const MAX_SPRITES = 32
95
+ const SPRITE_ATTR_Y = 0
96
+ const SPRITE_ATTR_X = 1
97
+ const SPRITE_ATTR_NAME = 2
98
+ const SPRITE_ATTR_COLOR = 3
99
+ const SPRITE_ATTR_BYTES = 4
100
+ const LAST_SPRITE_YPOS = 0xD0
101
+ const MAX_SCANLINE_SPRITES = 4
102
+
103
+ // Status register flags
104
+ const STATUS_INT = 0x80
105
+ const STATUS_5S = 0x40
106
+ const STATUS_COL = 0x20
107
+
108
+ // Register 0 bits
109
+ const TMS_R0_MODE_GRAPHICS_II = 0x02
110
+
111
+ // Register 1 bits
112
+ const TMS_R1_DISP_ACTIVE = 0x40
113
+ const TMS_R1_INT_ENABLE = 0x20
114
+ const TMS_R1_MODE_MULTICOLOR = 0x08
115
+ const TMS_R1_MODE_TEXT = 0x10
116
+ const TMS_R1_SPRITE_16 = 0x02
117
+ const TMS_R1_SPRITE_MAG2 = 0x01
118
+
119
+ // Register indices
120
+ const TMS_REG_0 = 0
121
+ const TMS_REG_1 = 1
122
+ const TMS_REG_NAME_TABLE = 2
123
+ const TMS_REG_COLOR_TABLE = 3
124
+ const TMS_REG_PATTERN_TABLE = 4
125
+ const TMS_REG_SPRITE_ATTR_TABLE = 5
126
+ const TMS_REG_SPRITE_PATT_TABLE = 6
127
+ const TMS_REG_FG_BG_COLOR = 7
128
+ const TMS_NUM_REGISTERS = 8
129
+
130
+ // Timing (NTSC)
131
+ const TOTAL_SCANLINES = 262
132
+ const FRAMES_PER_SECOND = 60
133
+ const CYCLES_PER_TICK = 128 // Must match Machine.ts ioTickInterval
134
+
135
+ // Border offsets (centering 256x192 in 320x240)
136
+ const BORDER_X = (DISPLAY_WIDTH - TMS_PIXELS_X) / 2 // 32
137
+ const BORDER_Y = (DISPLAY_HEIGHT - TMS_PIXELS_Y) / 2 // 24
138
+
139
+ export class VideoCard implements IO {
140
+
141
+ raiseIRQ = () => {}
142
+ raiseNMI = () => {}
143
+
144
+ // ---- VDP internal state ----
145
+
146
+ /** Eight write-only registers */
147
+ private registers = new Uint8Array(TMS_NUM_REGISTERS)
148
+
149
+ /** Status register (read-only from CPU side) */
150
+ private status: number = 0
151
+
152
+ /** Current VRAM address for CPU access (auto-increments) */
153
+ private currentAddress: number = 0
154
+
155
+ /** Address / register write stage (0 or 1) */
156
+ private regWriteStage: number = 0
157
+
158
+ /** Holds first stage byte written to the control port */
159
+ private regWriteStage0Value: number = 0
160
+
161
+ /** Read-ahead buffer for VRAM reads */
162
+ private readAheadBuffer: number = 0
163
+
164
+ /** Current display mode (derived from registers) */
165
+ private mode: TmsMode = TmsMode.GRAPHICS_I
166
+
167
+ /** 16 KB Video RAM */
168
+ private vram = new Uint8Array(VRAM_SIZE)
169
+
170
+ /** Per-pixel sprite collision mask for the current scanline */
171
+ private rowSpriteBits = new Uint8Array(TMS_PIXELS_X)
172
+
173
+ /** Temporary scanline pixel buffer (color palette indices) */
174
+ private scanlinePixels = new Uint8Array(TMS_PIXELS_X)
175
+
176
+ /** 320 × 240 RGBA output buffer for SDL rendering */
177
+ buffer: Buffer = Buffer.alloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * 4)
178
+
179
+ /** Cycle accumulator for scanline timing */
180
+ private cycleAccumulator: number = 0
181
+
182
+ /** Current scanline being processed (0 – 261) */
183
+ private currentScanline: number = 0
184
+
185
+ // ================================================================
186
+ // IO Interface
187
+ // ================================================================
188
+
189
+ read(address: number): number {
190
+ if (address & 1) {
191
+ return this.readStatus()
192
+ }
193
+ return this.readData()
194
+ }
195
+
196
+ write(address: number, data: number): void {
197
+ if (address & 1) {
198
+ this.writeAddr(data)
199
+ } else {
200
+ this.writeData(data)
201
+ }
202
+ }
203
+
204
+ tick(frequency: number): void {
205
+ const cyclesPerFrame = frequency / FRAMES_PER_SECOND
206
+ const cyclesPerScanline = cyclesPerFrame / TOTAL_SCANLINES
207
+
208
+ this.cycleAccumulator += CYCLES_PER_TICK
209
+
210
+ while (this.cycleAccumulator >= cyclesPerScanline) {
211
+ this.cycleAccumulator -= cyclesPerScanline
212
+ this.processScanline()
213
+ }
214
+ }
215
+
216
+ reset(_coldStart: boolean): void {
217
+ this.regWriteStage0Value = 0
218
+ this.currentAddress = 0
219
+ this.regWriteStage = 0
220
+ this.status = 0
221
+ this.readAheadBuffer = 0
222
+ this.registers.fill(0)
223
+ this.cycleAccumulator = 0
224
+ this.currentScanline = 0
225
+ this.updateMode()
226
+ // VRAM intentionally left in unknown state (matches C reference)
227
+ this.fillBackground()
228
+ }
229
+
230
+ // ================================================================
231
+ // VDP Data / Control Ports
232
+ // ================================================================
233
+
234
+ /**
235
+ * Write to the control (address / register) port.
236
+ * Two-stage write:
237
+ * Stage 0 – latches the low byte (address LSB or register value)
238
+ * Stage 1 – interprets the high byte:
239
+ * bit 7 set → register write (bits 0-2 = register index)
240
+ * bit 7 clear → address set (bit 6: 0 = read, 1 = write)
241
+ */
242
+ private writeAddr(data: number): void {
243
+ if (this.regWriteStage === 0) {
244
+ this.regWriteStage0Value = data
245
+ this.regWriteStage = 1
246
+ } else {
247
+ if (data & 0x80) {
248
+ // Register write
249
+ this.registers[data & 0x07] = this.regWriteStage0Value
250
+ this.updateMode()
251
+ } else {
252
+ // Address set
253
+ this.currentAddress = this.regWriteStage0Value | ((data & 0x3F) << 8)
254
+ if ((data & 0x40) === 0) {
255
+ // Read mode – pre-fetch byte and auto-increment
256
+ this.readAheadBuffer = this.vram[this.currentAddress & VRAM_MASK]
257
+ this.currentAddress++
258
+ }
259
+ }
260
+ this.regWriteStage = 0
261
+ }
262
+ }
263
+
264
+ /** Write data to VRAM at the current address (auto-increments) */
265
+ private writeData(data: number): void {
266
+ this.regWriteStage = 0
267
+ this.readAheadBuffer = data
268
+ this.vram[this.currentAddress & VRAM_MASK] = data
269
+ this.currentAddress++
270
+ }
271
+
272
+ /**
273
+ * Read the status register.
274
+ * Clears the status flags and resets the write stage.
275
+ */
276
+ private readStatus(): number {
277
+ const tmp = this.status
278
+ this.status = 0
279
+ this.regWriteStage = 0
280
+ return tmp
281
+ }
282
+
283
+ /** Read data from VRAM via the read-ahead buffer (auto-increments) */
284
+ private readData(): number {
285
+ this.regWriteStage = 0
286
+ const value = this.readAheadBuffer
287
+ this.readAheadBuffer = this.vram[this.currentAddress & VRAM_MASK]
288
+ this.currentAddress++
289
+ return value
290
+ }
291
+
292
+ // ================================================================
293
+ // Mode Detection
294
+ // ================================================================
295
+
296
+ private updateMode(): void {
297
+ if (this.registers[TMS_REG_0] & TMS_R0_MODE_GRAPHICS_II) {
298
+ this.mode = TmsMode.GRAPHICS_II
299
+ } else {
300
+ const bits = (this.registers[TMS_REG_1] & (TMS_R1_MODE_MULTICOLOR | TMS_R1_MODE_TEXT)) >> 3
301
+ switch (bits) {
302
+ case 1: this.mode = TmsMode.MULTICOLOR; break
303
+ case 2: this.mode = TmsMode.TEXT; break
304
+ default: this.mode = TmsMode.GRAPHICS_I; break
305
+ }
306
+ }
307
+ }
308
+
309
+ // ================================================================
310
+ // Table Address Helpers
311
+ // ================================================================
312
+
313
+ private nameTableAddr(): number {
314
+ return (this.registers[TMS_REG_NAME_TABLE] & 0x0F) << 10
315
+ }
316
+
317
+ private colorTableAddr(): number {
318
+ const mask = this.mode === TmsMode.GRAPHICS_II ? 0x80 : 0xFF
319
+ return (this.registers[TMS_REG_COLOR_TABLE] & mask) << 6
320
+ }
321
+
322
+ private patternTableAddr(): number {
323
+ const mask = this.mode === TmsMode.GRAPHICS_II ? 0x04 : 0x07
324
+ return (this.registers[TMS_REG_PATTERN_TABLE] & mask) << 11
325
+ }
326
+
327
+ private spriteAttrTableAddr(): number {
328
+ return (this.registers[TMS_REG_SPRITE_ATTR_TABLE] & 0x7F) << 7
329
+ }
330
+
331
+ private spritePatternTableAddr(): number {
332
+ return (this.registers[TMS_REG_SPRITE_PATT_TABLE] & 0x07) << 11
333
+ }
334
+
335
+ // ================================================================
336
+ // Color Helpers
337
+ // ================================================================
338
+
339
+ /** Backdrop / border color (low nibble of register 7) */
340
+ private mainBgColor(): number {
341
+ return this.registers[TMS_REG_FG_BG_COLOR] & 0x0F
342
+ }
343
+
344
+ /** Text-mode foreground (high nibble of register 7, transparent → backdrop) */
345
+ private mainFgColor(): number {
346
+ const c = this.registers[TMS_REG_FG_BG_COLOR] >> 4
347
+ return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
348
+ }
349
+
350
+ /** Foreground from a color byte (high nibble, transparent → backdrop) */
351
+ private fgColor(colorByte: number): number {
352
+ const c = colorByte >> 4
353
+ return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
354
+ }
355
+
356
+ /** Background from a color byte (low nibble, transparent → backdrop) */
357
+ private bgColor(colorByte: number): number {
358
+ const c = colorByte & 0x0F
359
+ return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
360
+ }
361
+
362
+ // ================================================================
363
+ // Sprite Helpers
364
+ // ================================================================
365
+
366
+ private spriteSize(): number {
367
+ return this.registers[TMS_REG_1] & TMS_R1_SPRITE_16 ? 16 : 8
368
+ }
369
+
370
+ private spriteMag(): boolean {
371
+ return !!(this.registers[TMS_REG_1] & TMS_R1_SPRITE_MAG2)
372
+ }
373
+
374
+ private displayEnabled(): boolean {
375
+ return !!(this.registers[TMS_REG_1] & TMS_R1_DISP_ACTIVE)
376
+ }
377
+
378
+ // ================================================================
379
+ // Timing / Scanline Processing
380
+ // ================================================================
381
+
382
+ private processScanline(): void {
383
+ if (this.currentScanline === 0) {
384
+ this.fillBackground()
385
+ }
386
+
387
+ if (this.currentScanline < TMS_PIXELS_Y) {
388
+ this.renderScanline(this.currentScanline)
389
+ }
390
+
391
+ this.currentScanline++
392
+ if (this.currentScanline >= TOTAL_SCANLINES) {
393
+ this.currentScanline = 0
394
+ }
395
+ }
396
+
397
+ // ================================================================
398
+ // Scanline Rendering
399
+ // ================================================================
400
+
401
+ private renderScanline(y: number): void {
402
+ const pixels = this.scanlinePixels
403
+
404
+ if (!this.displayEnabled() || y >= TMS_PIXELS_Y) {
405
+ pixels.fill(this.mainBgColor())
406
+ } else {
407
+ switch (this.mode) {
408
+ case TmsMode.GRAPHICS_I:
409
+ this.graphicsIScanLine(y, pixels)
410
+ break
411
+ case TmsMode.GRAPHICS_II:
412
+ this.graphicsIIScanLine(y, pixels)
413
+ break
414
+ case TmsMode.TEXT:
415
+ this.textScanLine(y, pixels)
416
+ break
417
+ case TmsMode.MULTICOLOR:
418
+ this.multicolorScanLine(y, pixels)
419
+ break
420
+ }
421
+ }
422
+
423
+ // Set interrupt flag at end of active display
424
+ if (y === TMS_PIXELS_Y - 1 && (this.registers[TMS_REG_1] & TMS_R1_INT_ENABLE)) {
425
+ this.status |= STATUS_INT
426
+ this.raiseIRQ()
427
+ }
428
+
429
+ this.writeScanlineToBuffer(y, pixels)
430
+ }
431
+
432
+ // ---- Graphics I ----
433
+
434
+ private graphicsIScanLine(y: number, pixels: Uint8Array): void {
435
+ const tileY = y >> 3
436
+ const pattRow = y & 0x07
437
+ const rowNamesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
438
+ const patternBase = this.patternTableAddr()
439
+ const colorBase = this.colorTableAddr()
440
+
441
+ for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
442
+ const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK]
443
+ let pattByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
444
+ const colorByte = this.vram[(colorBase + (pattIdx >>> 3)) & VRAM_MASK]
445
+
446
+ const fg = this.fgColor(colorByte)
447
+ const bg = this.bgColor(colorByte)
448
+
449
+ const base = tileX * GRAPHICS_CHAR_WIDTH
450
+ for (let bit = 0; bit < GRAPHICS_CHAR_WIDTH; bit++) {
451
+ pixels[base + bit] = (pattByte & 0x80) ? fg : bg
452
+ pattByte = (pattByte << 1) & 0xFF
453
+ }
454
+ }
455
+
456
+ this.outputSprites(y, pixels)
457
+ }
458
+
459
+ // ---- Graphics II ----
460
+
461
+ private graphicsIIScanLine(y: number, pixels: Uint8Array): void {
462
+ const tileY = y >> 3
463
+ const pattRow = y & 0x07
464
+ const rowNamesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
465
+
466
+ const nameMask = ((this.registers[TMS_REG_COLOR_TABLE] & 0x7F) << 3) | 0x07
467
+
468
+ const pageThird = ((tileY & 0x18) >> 3)
469
+ & (this.registers[TMS_REG_PATTERN_TABLE] & 0x03)
470
+ const pageOffset = pageThird << 11
471
+
472
+ const patternBase = this.patternTableAddr() + pageOffset
473
+ const colorBase = this.colorTableAddr()
474
+ + (pageOffset & ((this.registers[TMS_REG_COLOR_TABLE] & 0x60) << 6))
475
+
476
+ for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
477
+ const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK] & nameMask
478
+ const pattRowOffset = pattIdx * PATTERN_BYTES + pattRow
479
+ const pattByte = this.vram[(patternBase + pattRowOffset) & VRAM_MASK]
480
+ const colorByte = this.vram[(colorBase + pattRowOffset) & VRAM_MASK]
481
+
482
+ const fg = this.fgColor(colorByte)
483
+ const bg = this.bgColor(colorByte)
484
+
485
+ const base = tileX * GRAPHICS_CHAR_WIDTH
486
+ for (let bit = 0; bit < GRAPHICS_CHAR_WIDTH; bit++) {
487
+ pixels[base + bit] = ((pattByte << bit) & 0x80) ? fg : bg
488
+ }
489
+ }
490
+
491
+ this.outputSprites(y, pixels)
492
+ }
493
+
494
+ // ---- Text ----
495
+
496
+ private textScanLine(y: number, pixels: Uint8Array): void {
497
+ const tileY = y >> 3
498
+ const pattRow = y & 0x07
499
+ const rowNamesAddr = this.nameTableAddr() + tileY * TEXT_NUM_COLS
500
+ const patternBase = this.patternTableAddr()
501
+
502
+ const bg = this.mainBgColor()
503
+ const fg = this.mainFgColor()
504
+
505
+ // Left and right padding
506
+ for (let i = 0; i < TEXT_PADDING_PX; i++) {
507
+ pixels[i] = bg
508
+ pixels[TMS_PIXELS_X - TEXT_PADDING_PX + i] = bg
509
+ }
510
+
511
+ for (let tileX = 0; tileX < TEXT_NUM_COLS; tileX++) {
512
+ const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK]
513
+ const pattByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
514
+
515
+ for (let bit = 0; bit < TEXT_CHAR_WIDTH; bit++) {
516
+ pixels[TEXT_PADDING_PX + tileX * TEXT_CHAR_WIDTH + bit] =
517
+ ((pattByte << bit) & 0x80) ? fg : bg
518
+ }
519
+ }
520
+ // No sprites in Text mode
521
+ }
522
+
523
+ // ---- Multicolor ----
524
+
525
+ private multicolorScanLine(y: number, pixels: Uint8Array): void {
526
+ const tileY = y >> 3
527
+ const pattRow = (Math.floor(y / 4) & 0x01) + (tileY & 0x03) * 2
528
+ const namesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
529
+ const patternBase = this.patternTableAddr()
530
+
531
+ for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
532
+ const pattIdx = this.vram[(namesAddr + tileX) & VRAM_MASK]
533
+ const colorByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
534
+
535
+ const fg = this.fgColor(colorByte)
536
+ const bg = this.bgColor(colorByte)
537
+
538
+ const base = tileX * 8
539
+ for (let i = 0; i < 4; i++) pixels[base + i] = fg
540
+ for (let i = 4; i < 8; i++) pixels[base + i] = bg
541
+ }
542
+
543
+ this.outputSprites(y, pixels)
544
+ }
545
+
546
+ // ================================================================
547
+ // Sprite Rendering
548
+ // ================================================================
549
+
550
+ private outputSprites(y: number, pixels: Uint8Array): void {
551
+ const mag = this.spriteMag()
552
+ const sprite16 = this.spriteSize() === 16
553
+ const sprSize = this.spriteSize()
554
+ const spriteSizePx = sprSize * (mag ? 2 : 1)
555
+ const attrTableAddr = this.spriteAttrTableAddr()
556
+ const pattTableAddr = this.spritePatternTableAddr()
557
+
558
+ let spritesShown = 0
559
+
560
+ // Clear status at start of frame (matches C reference)
561
+ if (y === 0) {
562
+ this.status = 0
563
+ }
564
+
565
+ for (let spriteIdx = 0; spriteIdx < MAX_SPRITES; spriteIdx++) {
566
+ const attrBase = attrTableAddr + spriteIdx * SPRITE_ATTR_BYTES
567
+ let yPos: number = this.vram[(attrBase + SPRITE_ATTR_Y) & VRAM_MASK]
568
+
569
+ // Stop processing at sentinel value
570
+ if (yPos === LAST_SPRITE_YPOS) {
571
+ if ((this.status & STATUS_5S) === 0) {
572
+ this.status |= spriteIdx
573
+ }
574
+ break
575
+ }
576
+
577
+ // Handle wrap-around for sprites above the top of the screen
578
+ if (yPos > 0xE0) {
579
+ yPos -= 256
580
+ }
581
+
582
+ // First visible row is yPos + 1
583
+ yPos += 1
584
+
585
+ let pattRow = y - yPos
586
+ if (mag) {
587
+ pattRow >>= 1
588
+ }
589
+
590
+ // Skip sprite if not visible on this scanline
591
+ if (pattRow < 0 || pattRow >= sprSize) {
592
+ continue
593
+ }
594
+
595
+ // Clear collision mask on first visible sprite of this scanline
596
+ if (spritesShown === 0) {
597
+ this.rowSpriteBits.fill(0)
598
+ }
599
+
600
+ const spriteColor = this.vram[(attrBase + SPRITE_ATTR_COLOR) & VRAM_MASK] & 0x0F
601
+
602
+ // Check scanline sprite limit
603
+ spritesShown++
604
+ if (spritesShown > MAX_SCANLINE_SPRITES) {
605
+ if ((this.status & STATUS_5S) === 0) {
606
+ this.status |= STATUS_5S | spriteIdx
607
+ }
608
+ break
609
+ }
610
+
611
+ // Sprite pattern data
612
+ const pattIdx = this.vram[(attrBase + SPRITE_ATTR_NAME) & VRAM_MASK]
613
+ const pattOffset = pattTableAddr + pattIdx * PATTERN_BYTES + pattRow
614
+
615
+ // Early clock shifts sprite 32 pixels left
616
+ const earlyClockBit = this.vram[(attrBase + SPRITE_ATTR_COLOR) & VRAM_MASK] & 0x80
617
+ const earlyClockOffset = earlyClockBit ? -32 : 0
618
+ const xPos = this.vram[(attrBase + SPRITE_ATTR_X) & VRAM_MASK] + earlyClockOffset
619
+
620
+ let pattByte = this.vram[pattOffset & VRAM_MASK]
621
+ let screenBit = 0
622
+ let pattBit = 0
623
+
624
+ const endXPos = Math.min(xPos + spriteSizePx, TMS_PIXELS_X)
625
+
626
+ for (let screenX = xPos; screenX < endXPos; screenX++, screenBit++) {
627
+ if (screenX >= 0) {
628
+ // Check high bit of pattern byte
629
+ if (pattByte & 0x80) {
630
+ // Write pixel if sprite is non-transparent and no higher-priority non-transparent sprite already wrote here
631
+ if (spriteColor !== TmsColor.TRANSPARENT && this.rowSpriteBits[screenX] < 2) {
632
+ pixels[screenX] = spriteColor
633
+ }
634
+
635
+ // Collision detection
636
+ if (this.rowSpriteBits[screenX]) {
637
+ this.status |= STATUS_COL
638
+ } else {
639
+ this.rowSpriteBits[screenX] = spriteColor + 1
640
+ }
641
+ }
642
+ }
643
+
644
+ // Advance pattern bit (every pixel, or every other pixel if magnified)
645
+ if (!mag || (screenBit & 0x01)) {
646
+ pattByte = (pattByte << 1) & 0xFF
647
+ pattBit++
648
+ if (pattBit === GRAPHICS_CHAR_WIDTH && sprite16) {
649
+ // Switch from left half (A/B) to right half (C/D) of 16×16 sprite
650
+ pattBit = 0
651
+ pattByte = this.vram[(pattOffset + PATTERN_BYTES * 2) & VRAM_MASK]
652
+ }
653
+ }
654
+ }
655
+ }
656
+ }
657
+
658
+ // ================================================================
659
+ // Buffer Management
660
+ // ================================================================
661
+
662
+ /** Fill entire output buffer with the current backdrop color */
663
+ private fillBackground(): void {
664
+ const bgIdx = this.mainBgColor()
665
+ const [r, g, b, a] = TMS_PALETTE[bgIdx]
666
+ for (let i = 0; i < this.buffer.length; i += 4) {
667
+ this.buffer[i] = r
668
+ this.buffer[i + 1] = g
669
+ this.buffer[i + 2] = b
670
+ this.buffer[i + 3] = a
671
+ }
672
+ }
673
+
674
+ /** Write a rendered scanline into the output buffer at the correct position */
675
+ private writeScanlineToBuffer(y: number, pixels: Uint8Array): void {
676
+ const bufferY = y + BORDER_Y
677
+ if (bufferY < 0 || bufferY >= DISPLAY_HEIGHT) return
678
+
679
+ const rowOffset = bufferY * DISPLAY_WIDTH * 4
680
+ for (let x = 0; x < TMS_PIXELS_X; x++) {
681
+ const offset = rowOffset + (BORDER_X + x) * 4
682
+ const [r, g, b, a] = TMS_PALETTE[pixels[x] & 0x0F]
683
+ this.buffer[offset] = r
684
+ this.buffer[offset + 1] = g
685
+ this.buffer[offset + 2] = b
686
+ this.buffer[offset + 3] = a
687
+ }
688
+ }
689
+
690
+ // ================================================================
691
+ // Public Accessors (testing / debugging)
692
+ // ================================================================
693
+
694
+ /** Read a VDP register value */
695
+ getRegister(reg: number): number {
696
+ return this.registers[reg & 0x07]
697
+ }
698
+
699
+ /** Write a VDP register value directly (bypasses control-port staging) */
700
+ setRegister(reg: number, value: number): void {
701
+ this.registers[reg & 0x07] = value
702
+ this.updateMode()
703
+ }
704
+
705
+ /** Read a VRAM byte directly (does not affect read-ahead buffer) */
706
+ getVramByte(addr: number): number {
707
+ return this.vram[addr & VRAM_MASK]
708
+ }
709
+
710
+ /** Write a VRAM byte directly (does not affect address pointer) */
711
+ setVramByte(addr: number, value: number): void {
712
+ this.vram[addr & VRAM_MASK] = value
713
+ }
714
+
715
+ /** Peek at the status register without clearing it */
716
+ getStatus(): number {
717
+ return this.status
718
+ }
719
+
720
+ /** Get the current display mode */
721
+ getMode(): TmsMode {
722
+ return this.mode
723
+ }
724
+
725
+ /** Get the display-enabled state */
726
+ isDisplayEnabled(): boolean {
727
+ return this.displayEnabled()
728
+ }
729
+
730
+ }