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.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/components/CPU.js +1170 -0
- package/dist/components/CPU.js.map +1 -0
- package/dist/components/Cart.js +23 -0
- package/dist/components/Cart.js.map +1 -0
- package/dist/components/IO/Empty.js +19 -0
- package/dist/components/IO/Empty.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
- package/dist/components/IO/GPIOCard.js +597 -0
- package/dist/components/IO/GPIOCard.js.map +1 -0
- package/dist/components/IO/InputBoard.js +19 -0
- package/dist/components/IO/InputBoard.js.map +1 -0
- package/dist/components/IO/LCDCard.js +19 -0
- package/dist/components/IO/LCDCard.js.map +1 -0
- package/dist/components/IO/RAMCard.js +63 -0
- package/dist/components/IO/RAMCard.js.map +1 -0
- package/dist/components/IO/RTCCard.js +483 -0
- package/dist/components/IO/RTCCard.js.map +1 -0
- package/dist/components/IO/SerialCard.js +282 -0
- package/dist/components/IO/SerialCard.js.map +1 -0
- package/dist/components/IO/SoundCard.js +620 -0
- package/dist/components/IO/SoundCard.js.map +1 -0
- package/dist/components/IO/StorageCard.js +428 -0
- package/dist/components/IO/StorageCard.js.map +1 -0
- package/dist/components/IO/VGACard.js +9 -0
- package/dist/components/IO/VGACard.js.map +1 -0
- package/dist/components/IO/VideoCard.js +623 -0
- package/dist/components/IO/VideoCard.js.map +1 -0
- package/dist/components/IO.js +3 -0
- package/dist/components/IO.js.map +1 -0
- package/dist/components/Machine.js +310 -0
- package/dist/components/Machine.js.map +1 -0
- package/dist/components/RAM.js +24 -0
- package/dist/components/RAM.js.map +1 -0
- package/dist/components/ROM.js +23 -0
- package/dist/components/ROM.js.map +1 -0
- package/dist/index.js +441 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/CPU.test.js +1626 -0
- package/dist/tests/CPU.test.js.map +1 -0
- package/dist/tests/Cart.test.js +119 -0
- package/dist/tests/Cart.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOCard.test.js +503 -0
- package/dist/tests/IO/GPIOCard.test.js.map +1 -0
- package/dist/tests/IO/RAMCard.test.js +229 -0
- package/dist/tests/IO/RAMCard.test.js.map +1 -0
- package/dist/tests/IO/RTCCard.test.js +177 -0
- package/dist/tests/IO/RTCCard.test.js.map +1 -0
- package/dist/tests/IO/SerialCard.test.js +423 -0
- package/dist/tests/IO/SerialCard.test.js.map +1 -0
- package/dist/tests/IO/SoundCard.test.js +528 -0
- package/dist/tests/IO/SoundCard.test.js.map +1 -0
- package/dist/tests/IO/StorageCard.test.js +647 -0
- package/dist/tests/IO/StorageCard.test.js.map +1 -0
- package/dist/tests/IO/VideoCard.test.js +549 -0
- package/dist/tests/IO/VideoCard.test.js.map +1 -0
- package/dist/tests/Machine.test.js +383 -0
- package/dist/tests/Machine.test.js.map +1 -0
- package/dist/tests/RAM.test.js +160 -0
- package/dist/tests/RAM.test.js.map +1 -0
- package/dist/tests/ROM.test.js +123 -0
- package/dist/tests/ROM.test.js.map +1 -0
- package/jest.config.cjs +9 -0
- package/package.json +43 -0
- package/src/components/CPU.ts +1371 -0
- package/src/components/Cart.ts +20 -0
- package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
- package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
- package/src/components/IO/GPIOCard.ts +677 -0
- package/src/components/IO/RAMCard.ts +68 -0
- package/src/components/IO/RTCCard.ts +518 -0
- package/src/components/IO/SerialCard.ts +335 -0
- package/src/components/IO/SoundCard.ts +711 -0
- package/src/components/IO/StorageCard.ts +473 -0
- package/src/components/IO/VideoCard.ts +730 -0
- package/src/components/IO.ts +11 -0
- package/src/components/Machine.ts +364 -0
- package/src/components/RAM.ts +23 -0
- package/src/components/ROM.ts +19 -0
- package/src/index.ts +474 -0
- package/src/tests/CPU.test.ts +2045 -0
- package/src/tests/Cart.test.ts +149 -0
- package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
- package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
- package/src/tests/IO/GPIOCard.test.ts +644 -0
- package/src/tests/IO/RAMCard.test.ts +284 -0
- package/src/tests/IO/RTCCard.test.ts +222 -0
- package/src/tests/IO/SerialCard.test.ts +530 -0
- package/src/tests/IO/SoundCard.test.ts +659 -0
- package/src/tests/IO/StorageCard.test.ts +787 -0
- package/src/tests/IO/VideoCard.test.ts +668 -0
- package/src/tests/Machine.test.ts +437 -0
- package/src/tests/RAM.test.ts +196 -0
- package/src/tests/ROM.test.ts +154 -0
- 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
|
+
})
|