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,668 @@
1
+ import { VideoCard, TmsMode, TmsColor } from '../../components/IO/VideoCard'
2
+
3
+ /**
4
+ * Helper: write a register value through the control port (two-stage write)
5
+ */
6
+ const writeRegister = (vdp: VideoCard, reg: number, value: number): void => {
7
+ vdp.write(1, value) // Stage 0: register value
8
+ vdp.write(1, 0x80 | reg) // Stage 1: register number with bit 7 set
9
+ }
10
+
11
+ /**
12
+ * Helper: set VRAM write address through the control port
13
+ */
14
+ const setWriteAddress = (vdp: VideoCard, addr: number): void => {
15
+ vdp.write(1, addr & 0xFF) // Stage 0: address low byte
16
+ vdp.write(1, ((addr >> 8) & 0x3F) | 0x40) // Stage 1: address high + write flag
17
+ }
18
+
19
+ /**
20
+ * Helper: set VRAM read address through the control port
21
+ */
22
+ const setReadAddress = (vdp: VideoCard, addr: number): void => {
23
+ vdp.write(1, addr & 0xFF) // Stage 0: address low byte
24
+ vdp.write(1, (addr >> 8) & 0x3F) // Stage 1: address high (no write flag)
25
+ }
26
+
27
+ /**
28
+ * Helper: write a sequence of bytes to VRAM starting at an address
29
+ */
30
+ const writeVramBytes = (vdp: VideoCard, addr: number, bytes: number[]): void => {
31
+ setWriteAddress(vdp, addr)
32
+ for (const b of bytes) {
33
+ vdp.write(0, b) // Data port
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Helper: setup Graphics I mode with standard table addresses
39
+ */
40
+ const setupGraphicsI = (vdp: VideoCard): void => {
41
+ writeRegister(vdp, 0, 0x00) // No external VDP, Graphics I
42
+ writeRegister(vdp, 1, 0x60) // 16K, display active, interrupts enabled, Graphics I
43
+ writeRegister(vdp, 2, 0x0E) // Name table at 0x3800
44
+ writeRegister(vdp, 3, 0x00) // Color table at 0x0000
45
+ writeRegister(vdp, 4, 0x04) // Pattern table at 0x2000
46
+ writeRegister(vdp, 5, 0x76) // Sprite attr at 0x3B00
47
+ writeRegister(vdp, 6, 0x03) // Sprite pattern at 0x1800
48
+ writeRegister(vdp, 7, 0x17) // FG=black(1), BG=cyan(7)
49
+ }
50
+
51
+ /**
52
+ * Helper: setup Graphics II mode with standard table addresses
53
+ */
54
+ const setupGraphicsII = (vdp: VideoCard): void => {
55
+ writeRegister(vdp, 0, 0x02) // Graphics II mode
56
+ writeRegister(vdp, 1, 0x60) // 16K, display active, interrupts enabled
57
+ writeRegister(vdp, 2, 0x0E) // Name table at 0x3800
58
+ writeRegister(vdp, 3, 0x7F) // Color table mask
59
+ writeRegister(vdp, 4, 0x07) // Pattern table mask
60
+ writeRegister(vdp, 5, 0x76) // Sprite attr at 0x3B00
61
+ writeRegister(vdp, 6, 0x03) // Sprite pattern at 0x1800
62
+ writeRegister(vdp, 7, 0x17) // FG=black(1), BG=cyan(7)
63
+ }
64
+
65
+ /**
66
+ * Helper: setup Text mode
67
+ */
68
+ const setupTextMode = (vdp: VideoCard): void => {
69
+ writeRegister(vdp, 0, 0x00) // No external VDP
70
+ writeRegister(vdp, 1, 0x70) // 16K, display active, interrupts, Text mode
71
+ writeRegister(vdp, 2, 0x0E) // Name table at 0x3800
72
+ writeRegister(vdp, 4, 0x04) // Pattern table at 0x2000
73
+ writeRegister(vdp, 7, 0xF4) // FG=white(15), BG=dark blue(4)
74
+ }
75
+
76
+ /**
77
+ * Helper: tick enough times to render exactly one complete frame.
78
+ * Must not overshoot into the next frame (scanline 0 of the next
79
+ * frame clears the status register during sprite processing).
80
+ */
81
+ const renderOneFrame = (vdp: VideoCard, frequency: number = 2000000): void => {
82
+ // At 2MHz: cyclesPerFrame ≈ 33333, each tick = 128 cycles → ~261 ticks/frame
83
+ const ticksPerFrame = Math.ceil((frequency / 60) / 128)
84
+ for (let i = 0; i < ticksPerFrame; i++) {
85
+ vdp.tick(frequency)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Helper: clear sprite attribute table (set all Y positions to 0xD0 = stop)
91
+ */
92
+ const clearSprites = (vdp: VideoCard, spriteAttrAddr: number = 0x3B00): void => {
93
+ setWriteAddress(vdp, spriteAttrAddr)
94
+ for (let i = 0; i < 32; i++) {
95
+ vdp.write(0, 0xD0) // Y = stop sentinel
96
+ vdp.write(0, 0x00) // X
97
+ vdp.write(0, 0x00) // Name
98
+ vdp.write(0, 0x00) // Color
99
+ }
100
+ }
101
+
102
+ describe('VideoCard (TMS9918 VDP)', () => {
103
+ let vdp: VideoCard
104
+
105
+ beforeEach(() => {
106
+ vdp = new VideoCard()
107
+ })
108
+
109
+ // ================================================================
110
+ // Initialization & Reset
111
+ // ================================================================
112
+
113
+ describe('Initialization', () => {
114
+ it('should initialize with all registers zeroed', () => {
115
+ for (let i = 0; i < 8; i++) {
116
+ expect(vdp.getRegister(i)).toBe(0)
117
+ }
118
+ })
119
+
120
+ it('should initialize in Graphics I mode', () => {
121
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_I)
122
+ })
123
+
124
+ it('should initialize with display disabled', () => {
125
+ expect(vdp.isDisplayEnabled()).toBe(false)
126
+ })
127
+
128
+ it('should initialize status register to 0', () => {
129
+ expect(vdp.getStatus()).toBe(0)
130
+ })
131
+
132
+ it('should have a 320x240 RGBA output buffer', () => {
133
+ expect(vdp.buffer.length).toBe(320 * 240 * 4)
134
+ })
135
+ })
136
+
137
+ describe('Reset', () => {
138
+ it('should clear registers on reset', () => {
139
+ writeRegister(vdp, 1, 0x60)
140
+ writeRegister(vdp, 7, 0xF1)
141
+ vdp.reset(true)
142
+ for (let i = 0; i < 8; i++) {
143
+ expect(vdp.getRegister(i)).toBe(0)
144
+ }
145
+ })
146
+
147
+ it('should clear status register on reset', () => {
148
+ // Trigger an interrupt
149
+ writeRegister(vdp, 1, 0x60)
150
+ renderOneFrame(vdp)
151
+ expect(vdp.getStatus() & 0x80).toBeTruthy()
152
+
153
+ vdp.reset(true)
154
+ expect(vdp.getStatus()).toBe(0)
155
+ })
156
+
157
+ it('should reset write stage on reset', () => {
158
+ // Write only the first stage byte
159
+ vdp.write(1, 0x42) // Stage 0 only
160
+ vdp.reset(true)
161
+ // Now writing two bytes should work correctly as a fresh two-stage write
162
+ writeRegister(vdp, 7, 0xAB)
163
+ expect(vdp.getRegister(7)).toBe(0xAB)
164
+ })
165
+ })
166
+
167
+ // ================================================================
168
+ // Register Read/Write
169
+ // ================================================================
170
+
171
+ describe('Register Access', () => {
172
+ it('should write and read back register values', () => {
173
+ for (let reg = 0; reg < 8; reg++) {
174
+ writeRegister(vdp, reg, 0x55 + reg)
175
+ expect(vdp.getRegister(reg)).toBe(0x55 + reg)
176
+ }
177
+ })
178
+
179
+ it('should mask register index to 3 bits', () => {
180
+ writeRegister(vdp, 0x08, 0xAA) // reg 8 → reg 0
181
+ expect(vdp.getRegister(0)).toBe(0xAA)
182
+ })
183
+
184
+ it('should update display mode on register write', () => {
185
+ // Graphics II: reg 0 bit 1
186
+ writeRegister(vdp, 0, 0x02)
187
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_II)
188
+
189
+ // Text: reg 1 bit 4
190
+ writeRegister(vdp, 0, 0x00)
191
+ writeRegister(vdp, 1, 0x10)
192
+ expect(vdp.getMode()).toBe(TmsMode.TEXT)
193
+
194
+ // Multicolor: reg 1 bit 3
195
+ writeRegister(vdp, 1, 0x08)
196
+ expect(vdp.getMode()).toBe(TmsMode.MULTICOLOR)
197
+
198
+ // Graphics I: no special bits
199
+ writeRegister(vdp, 0, 0x00)
200
+ writeRegister(vdp, 1, 0x00)
201
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_I)
202
+ })
203
+ })
204
+
205
+ // ================================================================
206
+ // VRAM Access
207
+ // ================================================================
208
+
209
+ describe('VRAM Access', () => {
210
+ it('should write and read VRAM data', () => {
211
+ setWriteAddress(vdp, 0x0000)
212
+ vdp.write(0, 0x42)
213
+ vdp.write(0, 0x43)
214
+ vdp.write(0, 0x44)
215
+
216
+ // Read back
217
+ setReadAddress(vdp, 0x0000)
218
+ expect(vdp.read(0)).toBe(0x42) // Pre-fetched during address set
219
+ expect(vdp.read(0)).toBe(0x43) // Next byte
220
+ expect(vdp.read(0)).toBe(0x44)
221
+ })
222
+
223
+ it('should auto-increment address on write', () => {
224
+ setWriteAddress(vdp, 0x1000)
225
+ for (let i = 0; i < 10; i++) {
226
+ vdp.write(0, i)
227
+ }
228
+
229
+ // Verify the bytes were written sequentially
230
+ for (let i = 0; i < 10; i++) {
231
+ expect(vdp.getVramByte(0x1000 + i)).toBe(i)
232
+ }
233
+ })
234
+
235
+ it('should auto-increment address on read', () => {
236
+ // Write sequential values
237
+ for (let i = 0; i < 5; i++) {
238
+ vdp.setVramByte(0x2000 + i, 0xA0 + i)
239
+ }
240
+
241
+ setReadAddress(vdp, 0x2000)
242
+ for (let i = 0; i < 5; i++) {
243
+ expect(vdp.read(0)).toBe(0xA0 + i)
244
+ }
245
+ })
246
+
247
+ it('should implement read-ahead buffer correctly', () => {
248
+ vdp.setVramByte(0x0000, 0x11)
249
+ vdp.setVramByte(0x0001, 0x22)
250
+ vdp.setVramByte(0x0002, 0x33)
251
+
252
+ // Setting read address pre-fetches first byte
253
+ setReadAddress(vdp, 0x0000)
254
+ // First read returns the pre-fetched byte (0x11), and fetches next (0x22)
255
+ expect(vdp.read(0)).toBe(0x11)
256
+ // Second read returns 0x22 (previously fetched), fetches 0x33
257
+ expect(vdp.read(0)).toBe(0x22)
258
+ expect(vdp.read(0)).toBe(0x33)
259
+ })
260
+
261
+ it('should wrap VRAM address at 16KB boundary', () => {
262
+ // Write at the end of VRAM
263
+ setWriteAddress(vdp, 0x3FFF)
264
+ vdp.write(0, 0xEE)
265
+ // Next write should wrap to 0x0000
266
+ vdp.write(0, 0xFF)
267
+
268
+ expect(vdp.getVramByte(0x3FFF)).toBe(0xEE)
269
+ expect(vdp.getVramByte(0x0000)).toBe(0xFF)
270
+ })
271
+
272
+ it('should reset write stage on data port operations', () => {
273
+ // Start a control port write (stage 0)
274
+ vdp.write(1, 0x42) // Stage 0
275
+
276
+ // A data write should reset the write stage
277
+ setWriteAddress(vdp, 0x0000) // Need address set first
278
+ vdp.write(0, 0x55)
279
+
280
+ // Now a full two-stage register write should work
281
+ writeRegister(vdp, 7, 0xCC)
282
+ expect(vdp.getRegister(7)).toBe(0xCC)
283
+ })
284
+ })
285
+
286
+ // ================================================================
287
+ // Status Register
288
+ // ================================================================
289
+
290
+ describe('Status Register', () => {
291
+ it('should read and clear status register', () => {
292
+ // Enable display and interrupts
293
+ writeRegister(vdp, 1, 0x60)
294
+ clearSprites(vdp)
295
+
296
+ renderOneFrame(vdp)
297
+
298
+ // Status should have interrupt flag
299
+ const status = vdp.read(1) // Read status through control port
300
+ expect(status & 0x80).toBeTruthy()
301
+
302
+ // Reading status should have cleared it
303
+ expect(vdp.getStatus()).toBe(0)
304
+ })
305
+
306
+ it('should reset write stage on status read', () => {
307
+ // Start a control port write (stage 0)
308
+ vdp.write(1, 0x42) // Stage 0
309
+
310
+ // Reading status should reset the write stage
311
+ vdp.read(1)
312
+
313
+ // Now a full two-stage register write should work
314
+ writeRegister(vdp, 7, 0xDD)
315
+ expect(vdp.getRegister(7)).toBe(0xDD)
316
+ })
317
+ })
318
+
319
+ // ================================================================
320
+ // Mode Detection
321
+ // ================================================================
322
+
323
+ describe('Mode Detection', () => {
324
+ it('should detect Graphics I mode', () => {
325
+ writeRegister(vdp, 0, 0x00)
326
+ writeRegister(vdp, 1, 0x00)
327
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_I)
328
+ })
329
+
330
+ it('should detect Graphics II mode (reg 0 bit 1)', () => {
331
+ writeRegister(vdp, 0, 0x02)
332
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_II)
333
+ })
334
+
335
+ it('should detect Text mode (reg 1 bit 4)', () => {
336
+ writeRegister(vdp, 0, 0x00)
337
+ writeRegister(vdp, 1, 0x10)
338
+ expect(vdp.getMode()).toBe(TmsMode.TEXT)
339
+ })
340
+
341
+ it('should detect Multicolor mode (reg 1 bit 3)', () => {
342
+ writeRegister(vdp, 0, 0x00)
343
+ writeRegister(vdp, 1, 0x08)
344
+ expect(vdp.getMode()).toBe(TmsMode.MULTICOLOR)
345
+ })
346
+
347
+ it('should prioritize Graphics II over other modes', () => {
348
+ writeRegister(vdp, 0, 0x02)
349
+ writeRegister(vdp, 1, 0x10) // Also set Text bit
350
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_II)
351
+ })
352
+ })
353
+
354
+ // ================================================================
355
+ // Display-Enabled Flag
356
+ // ================================================================
357
+
358
+ describe('Display Enable', () => {
359
+ it('should report display disabled when BLANK bit is clear', () => {
360
+ writeRegister(vdp, 1, 0x00) // Display inactive
361
+ expect(vdp.isDisplayEnabled()).toBe(false)
362
+ })
363
+
364
+ it('should report display enabled when BLANK bit is set', () => {
365
+ writeRegister(vdp, 1, 0x40) // Display active
366
+ expect(vdp.isDisplayEnabled()).toBe(true)
367
+ })
368
+ })
369
+
370
+ // ================================================================
371
+ // Interrupt Generation
372
+ // ================================================================
373
+
374
+ describe('Interrupt Generation', () => {
375
+ it('should set interrupt flag after rendering active display', () => {
376
+ writeRegister(vdp, 1, 0x60) // Display active + interrupts enabled
377
+ clearSprites(vdp)
378
+
379
+ renderOneFrame(vdp)
380
+
381
+ expect(vdp.getStatus() & 0x80).toBeTruthy()
382
+ })
383
+
384
+ it('should not set interrupt flag when interrupts are disabled', () => {
385
+ writeRegister(vdp, 1, 0x40) // Display active, interrupts disabled
386
+
387
+ renderOneFrame(vdp)
388
+
389
+ expect(vdp.getStatus() & 0x80).toBe(0)
390
+ })
391
+
392
+ it('should call raiseIRQ when interrupt flag is set', () => {
393
+ const irqFn = jest.fn()
394
+ vdp.raiseIRQ = irqFn
395
+
396
+ writeRegister(vdp, 1, 0x60) // Display active + interrupts enabled
397
+ clearSprites(vdp)
398
+
399
+ renderOneFrame(vdp)
400
+
401
+ expect(irqFn).toHaveBeenCalled()
402
+ })
403
+ })
404
+
405
+ // ================================================================
406
+ // Graphics I Rendering
407
+ // ================================================================
408
+
409
+ describe('Graphics I Mode Rendering', () => {
410
+ it('should render a tile with pattern data', () => {
411
+ setupGraphicsI(vdp)
412
+ clearSprites(vdp)
413
+
414
+ // Set name table entry: tile 0 at position (0,0)
415
+ vdp.setVramByte(0x3800, 0x00) // Name table: tile index 0
416
+
417
+ // Set a simple pattern for tile 0 (alternating lines)
418
+ // Pattern table at 0x2000
419
+ vdp.setVramByte(0x2000, 0xFF) // Row 0: all pixels on
420
+ vdp.setVramByte(0x2001, 0x00) // Row 1: all pixels off
421
+ vdp.setVramByte(0x2002, 0xFF) // Row 2: all pixels on
422
+ vdp.setVramByte(0x2003, 0x00) // Row 3: all pixels off
423
+ vdp.setVramByte(0x2004, 0xFF) // Row 4: all pixels on
424
+ vdp.setVramByte(0x2005, 0x00) // Row 5: all pixels off
425
+ vdp.setVramByte(0x2006, 0xFF) // Row 6: all pixels on
426
+ vdp.setVramByte(0x2007, 0x00) // Row 7: all pixels off
427
+
428
+ // Set color for tile 0 (group 0, indices 0-7)
429
+ // Color table at 0x0000, each entry covers 8 tiles
430
+ // FG = white (0xF), BG = black (0x1) → 0xF1
431
+ vdp.setVramByte(0x0000, 0xF1)
432
+
433
+ renderOneFrame(vdp)
434
+
435
+ // Check pixel at (0,0) in the active area → should be FG color (white = 15)
436
+ // Buffer position: (BORDER_X, BORDER_Y) = (32, 24) in RGBA
437
+ const offset = (24 * 320 + 32) * 4
438
+ // White = (0xFF, 0xFF, 0xFF, 0xFF)
439
+ expect(vdp.buffer[offset]).toBe(0xFF)
440
+ expect(vdp.buffer[offset + 1]).toBe(0xFF)
441
+ expect(vdp.buffer[offset + 2]).toBe(0xFF)
442
+ expect(vdp.buffer[offset + 3]).toBe(0xFF)
443
+
444
+ // Row 1 (pattern byte = 0x00) should be BG color (black = 1)
445
+ const offsetRow1 = (25 * 320 + 32) * 4
446
+ expect(vdp.buffer[offsetRow1]).toBe(0x00)
447
+ expect(vdp.buffer[offsetRow1 + 1]).toBe(0x00)
448
+ expect(vdp.buffer[offsetRow1 + 2]).toBe(0x00)
449
+ expect(vdp.buffer[offsetRow1 + 3]).toBe(0xFF)
450
+ })
451
+ })
452
+
453
+ // ================================================================
454
+ // Text Mode Rendering
455
+ // ================================================================
456
+
457
+ describe('Text Mode Rendering', () => {
458
+ it('should render left and right padding with background color', () => {
459
+ setupTextMode(vdp)
460
+
461
+ renderOneFrame(vdp)
462
+
463
+ // Left padding: first 8 pixels should be BG color (dark blue = 4)
464
+ // Dark blue palette: [0x54, 0x55, 0xED, 0xFF]
465
+ const offset = (24 * 320 + 32) * 4 // First active pixel in buffer
466
+ expect(vdp.buffer[offset]).toBe(0x54) // R
467
+ expect(vdp.buffer[offset + 1]).toBe(0x55) // G
468
+ expect(vdp.buffer[offset + 2]).toBe(0xED) // B
469
+ expect(vdp.buffer[offset + 3]).toBe(0xFF) // A
470
+
471
+ // Right padding: last 8 pixels of active area
472
+ const rightPaddingX = 32 + 248 // BORDER_X + (256 - 8)
473
+ const offsetRight = (24 * 320 + rightPaddingX) * 4
474
+ expect(vdp.buffer[offsetRight]).toBe(0x54)
475
+ expect(vdp.buffer[offsetRight + 1]).toBe(0x55)
476
+ expect(vdp.buffer[offsetRight + 2]).toBe(0xED)
477
+ })
478
+ })
479
+
480
+ // ================================================================
481
+ // Border Rendering
482
+ // ================================================================
483
+
484
+ describe('Border Rendering', () => {
485
+ it('should fill border with backdrop color', () => {
486
+ writeRegister(vdp, 1, 0x40) // Display active
487
+ writeRegister(vdp, 7, 0x07) // BG = cyan (7)
488
+
489
+ renderOneFrame(vdp)
490
+
491
+ // Check top-left corner (border area)
492
+ // Cyan palette: [0x43, 0xEB, 0xF6, 0xFF]
493
+ expect(vdp.buffer[0]).toBe(0x43)
494
+ expect(vdp.buffer[1]).toBe(0xEB)
495
+ expect(vdp.buffer[2]).toBe(0xF6)
496
+ expect(vdp.buffer[3]).toBe(0xFF)
497
+ })
498
+
499
+ it('should use black for transparent backdrop', () => {
500
+ writeRegister(vdp, 1, 0x40) // Display active
501
+ writeRegister(vdp, 7, 0x00) // BG = transparent (0)
502
+
503
+ renderOneFrame(vdp)
504
+
505
+ // Transparent renders as opaque black
506
+ expect(vdp.buffer[0]).toBe(0x00)
507
+ expect(vdp.buffer[1]).toBe(0x00)
508
+ expect(vdp.buffer[2]).toBe(0x00)
509
+ expect(vdp.buffer[3]).toBe(0xFF)
510
+ })
511
+ })
512
+
513
+ // ================================================================
514
+ // Blanked Display
515
+ // ================================================================
516
+
517
+ describe('Blanked Display', () => {
518
+ it('should fill active area with backdrop when display is disabled', () => {
519
+ writeRegister(vdp, 1, 0x20) // Display disabled, interrupts enabled
520
+ writeRegister(vdp, 7, 0x04) // BG = dark blue (4)
521
+
522
+ renderOneFrame(vdp)
523
+
524
+ // Active area pixel should be backdrop color
525
+ const offset = (24 * 320 + 32) * 4
526
+ // Dark blue: [0x54, 0x55, 0xED, 0xFF]
527
+ expect(vdp.buffer[offset]).toBe(0x54)
528
+ expect(vdp.buffer[offset + 1]).toBe(0x55)
529
+ expect(vdp.buffer[offset + 2]).toBe(0xED)
530
+ })
531
+ })
532
+
533
+ // ================================================================
534
+ // Sprite Processing
535
+ // ================================================================
536
+
537
+ describe('Sprite Processing', () => {
538
+ beforeEach(() => {
539
+ setupGraphicsI(vdp)
540
+ clearSprites(vdp)
541
+ })
542
+
543
+ it('should render a simple 8x8 sprite', () => {
544
+ // Sprite pattern at 0x1800 (sprite pattern table)
545
+ // Pattern 0: solid 8x8 block
546
+ for (let row = 0; row < 8; row++) {
547
+ vdp.setVramByte(0x1800 + row, 0xFF) // All pixels set
548
+ }
549
+
550
+ // Sprite 0 attribute: Y=0, X=0, Name=0, Color=white(15)
551
+ vdp.setVramByte(0x3B00 + 0, 0xFF) // Y = 0xFF → yPos becomes 0 (+1 offset)
552
+ vdp.setVramByte(0x3B00 + 1, 0x00) // X = 0
553
+ vdp.setVramByte(0x3B00 + 2, 0x00) // Name = 0
554
+ vdp.setVramByte(0x3B00 + 3, 0x0F) // Color = 15 (white)
555
+
556
+ // Sentinel for sprite 1
557
+ vdp.setVramByte(0x3B00 + 4, 0xD0)
558
+
559
+ renderOneFrame(vdp)
560
+
561
+ // Check pixel at sprite position (0,0) in active area
562
+ const offset = (24 * 320 + 32) * 4
563
+ // White overlay: [0xFF, 0xFF, 0xFF, 0xFF]
564
+ expect(vdp.buffer[offset]).toBe(0xFF)
565
+ expect(vdp.buffer[offset + 1]).toBe(0xFF)
566
+ expect(vdp.buffer[offset + 2]).toBe(0xFF)
567
+ expect(vdp.buffer[offset + 3]).toBe(0xFF)
568
+ })
569
+
570
+ it('should stop processing sprites at Y = 0xD0 sentinel', () => {
571
+ // Sprite 0: sentinel
572
+ vdp.setVramByte(0x3B00 + 0, 0xD0)
573
+
574
+ // Sprite 1: should not be processed
575
+ vdp.setVramByte(0x3B00 + 4, 0x00)
576
+ vdp.setVramByte(0x3B00 + 5, 0x00)
577
+ vdp.setVramByte(0x3B00 + 6, 0x00)
578
+ vdp.setVramByte(0x3B00 + 7, 0x0F)
579
+
580
+ // Pattern for sprite 1
581
+ for (let row = 0; row < 8; row++) {
582
+ vdp.setVramByte(0x1800 + row, 0xFF)
583
+ }
584
+
585
+ renderOneFrame(vdp)
586
+
587
+ // Pixel should NOT be white (sprite 1 not rendered)
588
+ const offset = (25 * 320 + 32) * 4
589
+ expect(vdp.buffer[offset]).not.toBe(0xFF)
590
+ })
591
+
592
+ it('should detect sprite collision (STATUS_COL)', () => {
593
+ // Two sprites overlapping at the same position
594
+ // Sprite 0: Y=0, X=0
595
+ vdp.setVramByte(0x3B00 + 0, 0xFF) // Y → 0
596
+ vdp.setVramByte(0x3B00 + 1, 0x00) // X = 0
597
+ vdp.setVramByte(0x3B00 + 2, 0x00) // Name = 0
598
+ vdp.setVramByte(0x3B00 + 3, 0x0F) // Color = 15
599
+
600
+ // Sprite 1: Y=0, X=0 (overlapping)
601
+ vdp.setVramByte(0x3B00 + 4, 0xFF) // Y → 0
602
+ vdp.setVramByte(0x3B00 + 5, 0x00) // X = 0
603
+ vdp.setVramByte(0x3B00 + 6, 0x00) // Name = 0
604
+ vdp.setVramByte(0x3B00 + 7, 0x0E) // Color = 14 (grey)
605
+
606
+ // Sentinel
607
+ vdp.setVramByte(0x3B00 + 8, 0xD0)
608
+
609
+ // Pattern 0: at least one pixel set
610
+ vdp.setVramByte(0x1800, 0x80) // Top-left pixel
611
+
612
+ renderOneFrame(vdp)
613
+
614
+ expect(vdp.getStatus() & 0x20).toBeTruthy() // STATUS_COL
615
+ })
616
+
617
+ it('should set 5th sprite flag when more than 4 sprites on a scanline', () => {
618
+ // Place 5 sprites on scanline 0
619
+ for (let i = 0; i < 5; i++) {
620
+ const base = 0x3B00 + i * 4
621
+ vdp.setVramByte(base + 0, 0xFF) // Y → 0
622
+ vdp.setVramByte(base + 1, i * 16) // X = spaced apart
623
+ vdp.setVramByte(base + 2, 0x00) // Name = 0
624
+ vdp.setVramByte(base + 3, 0x0F) // Color = 15
625
+ }
626
+
627
+ // Sentinel after sprite 5
628
+ vdp.setVramByte(0x3B00 + 20, 0xD0)
629
+
630
+ // Pattern: all pixels set
631
+ for (let row = 0; row < 8; row++) {
632
+ vdp.setVramByte(0x1800 + row, 0xFF)
633
+ }
634
+
635
+ renderOneFrame(vdp)
636
+
637
+ const status = vdp.getStatus()
638
+ expect(status & 0x40).toBeTruthy() // STATUS_5S flag
639
+ expect(status & 0x1F).toBe(4) // 5th sprite index
640
+ })
641
+ })
642
+
643
+ // ================================================================
644
+ // Direct Accessor Methods
645
+ // ================================================================
646
+
647
+ describe('Direct Accessors', () => {
648
+ it('should read/write VRAM directly', () => {
649
+ vdp.setVramByte(0x1234, 0xAB)
650
+ expect(vdp.getVramByte(0x1234)).toBe(0xAB)
651
+ })
652
+
653
+ it('should mask VRAM address to 14 bits', () => {
654
+ vdp.setVramByte(0xFFFF, 0xCD)
655
+ expect(vdp.getVramByte(0x3FFF)).toBe(0xCD)
656
+ })
657
+
658
+ it('should read/write registers directly', () => {
659
+ vdp.setRegister(3, 0xFF)
660
+ expect(vdp.getRegister(3)).toBe(0xFF)
661
+ })
662
+
663
+ it('should update mode when setting register directly', () => {
664
+ vdp.setRegister(0, 0x02)
665
+ expect(vdp.getMode()).toBe(TmsMode.GRAPHICS_II)
666
+ })
667
+ })
668
+ })