|
| 1 | +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; |
| 2 | +import { Video, HDISPENABLE, VDISPENABLE, USERDISPENABLE, EVERYTHINGENABLED } from "../../src/video.js"; |
| 3 | +import * as utils from "../../src/utils.js"; |
| 4 | + |
| 5 | +// Setup the video with imported constants |
| 6 | +describe("Video", () => { |
| 7 | + let video; |
| 8 | + let mockCpu; |
| 9 | + let mockVia; |
| 10 | + let mockFb32; |
| 11 | + let mockPaintExt; |
| 12 | + let mockTeletext; |
| 13 | + |
| 14 | + beforeEach(() => { |
| 15 | + // Reset all mocks |
| 16 | + vi.clearAllMocks(); |
| 17 | + |
| 18 | + // Mock frame buffer |
| 19 | + mockFb32 = new Uint32Array(1024 * 768); |
| 20 | + |
| 21 | + // Mock CPU with videoRead method |
| 22 | + mockCpu = { |
| 23 | + videoRead: vi.fn().mockReturnValue(0), |
| 24 | + interrupt: 0, |
| 25 | + }; |
| 26 | + |
| 27 | + // Mock VIA with cb2changecallback property |
| 28 | + mockVia = { |
| 29 | + cb2changecallback: null, |
| 30 | + setVBlankInt: vi.fn(), |
| 31 | + }; |
| 32 | + |
| 33 | + // Mock paint_ext function |
| 34 | + mockPaintExt = vi.fn(); |
| 35 | + |
| 36 | + // Spy on utils.makeFast32 |
| 37 | + vi.spyOn(utils, "makeFast32").mockImplementation((arr) => arr); |
| 38 | + |
| 39 | + // Create a video instance (using Model B mode, not Master) |
| 40 | + video = new Video(false, mockFb32, mockPaintExt); |
| 41 | + |
| 42 | + // Create the mock teletext manually and replace the one in the video object |
| 43 | + mockTeletext = { |
| 44 | + setDEW: vi.fn(), |
| 45 | + setDISPTMG: vi.fn(), |
| 46 | + setRA0: vi.fn(), |
| 47 | + fetchData: vi.fn(), |
| 48 | + render: vi.fn(), |
| 49 | + }; |
| 50 | + |
| 51 | + // Replace the teletext instance |
| 52 | + video.teletext = mockTeletext; |
| 53 | + |
| 54 | + // Reset and connect CPU and VIA |
| 55 | + video.reset(mockCpu, mockVia); |
| 56 | + }); |
| 57 | + |
| 58 | + afterEach(() => { |
| 59 | + vi.restoreAllMocks(); |
| 60 | + }); |
| 61 | + |
| 62 | + describe("ULA control register", () => { |
| 63 | + it("should set teletextMode when bit 1 is set", () => { |
| 64 | + // Initially teletext mode should be false |
| 65 | + expect(video.teletextMode).toBe(false); |
| 66 | + |
| 67 | + // Write to ULA control register (address 0) with value 2 (bit 1 set) |
| 68 | + video.ula.write(0, 2); |
| 69 | + |
| 70 | + // Verify teletext mode was set |
| 71 | + expect(video.teletextMode).toBe(true); |
| 72 | + |
| 73 | + // Clear bit 1 |
| 74 | + video.ula.write(0, 0); |
| 75 | + |
| 76 | + // Verify teletext mode was cleared |
| 77 | + expect(video.teletextMode).toBe(false); |
| 78 | + }); |
| 79 | + }); |
| 80 | + |
| 81 | + describe("Memory access in teletext mode", () => { |
| 82 | + beforeEach(() => { |
| 83 | + // Set teletext mode |
| 84 | + video.ula.write(0, 2); |
| 85 | + }); |
| 86 | + |
| 87 | + it("should use correct addressing for Mode 7 on Master", () => { |
| 88 | + // Set up MA13 set (addr bit 13 set) for Mode 7 addressing |
| 89 | + video.addr = 0x2000; // Bit 13 set |
| 90 | + video.isMaster = true; // Set to Master mode |
| 91 | + |
| 92 | + // Set up CPU to return a specific value |
| 93 | + const expectedData = 0x7f; |
| 94 | + mockCpu.videoRead.mockReturnValue(expectedData); |
| 95 | + |
| 96 | + // Call readVideoMem which should use chunky addressing mode |
| 97 | + const result = video.readVideoMem(); |
| 98 | + |
| 99 | + // Verify result |
| 100 | + expect(result).toBe(expectedData); |
| 101 | + |
| 102 | + // Check correct address was used (should mask to 0x3ff and add 0x7c00 for Master) |
| 103 | + expect(mockCpu.videoRead).toHaveBeenCalledWith(0x7c00); |
| 104 | + }); |
| 105 | + |
| 106 | + it("should handle Model B quirk for reading 0x3c00", () => { |
| 107 | + // Set up addr with MA13 set but MA11 clear for Model B quirk |
| 108 | + video.addr = 0x2000; // Bit 13 set, bit 11 clear |
| 109 | + video.isMaster = false; // Set to Model B mode |
| 110 | + |
| 111 | + // Call readVideoMem |
| 112 | + video.readVideoMem(); |
| 113 | + |
| 114 | + // For Model B, should use 0x3c00 instead of 0x7c00 |
| 115 | + expect(mockCpu.videoRead).toHaveBeenCalledWith(0x3c00); |
| 116 | + }); |
| 117 | + }); |
| 118 | + |
| 119 | + describe("Teletext integration", () => { |
| 120 | + beforeEach(() => { |
| 121 | + // Set teletext mode |
| 122 | + video.ula.write(0, 2); |
| 123 | + expect(video.teletextMode).toBe(true); |
| 124 | + }); |
| 125 | + |
| 126 | + it("should call teletext.setDISPTMG when display enable state changes", () => { |
| 127 | + // Clear the teletext mock history |
| 128 | + mockTeletext.setDISPTMG.mockClear(); |
| 129 | + |
| 130 | + // Test display enable set - all required display flags set |
| 131 | + video.dispEnabled = 0; |
| 132 | + video.dispEnableSet(HDISPENABLE | VDISPENABLE | USERDISPENABLE); |
| 133 | + |
| 134 | + // The mask in dispEnableChanged is HDISPENABLE | VDISPENABLE | USERDISPENABLE |
| 135 | + // and it checks if all bits are set with (this.dispEnabled & mask) === mask |
| 136 | + expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(true); |
| 137 | + |
| 138 | + // Clear the mock history |
| 139 | + mockTeletext.setDISPTMG.mockClear(); |
| 140 | + |
| 141 | + // Test display enable clear - clear a required flag |
| 142 | + video.dispEnableClear(HDISPENABLE); |
| 143 | + |
| 144 | + // Now the mask check will fail, so setDISPTMG is called with false |
| 145 | + expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(false); |
| 146 | + }); |
| 147 | + |
| 148 | + it("should update teletext.setRA0 correctly based on scanlineCounter", () => { |
| 149 | + // Initialize scanlineCounter to 0 |
| 150 | + video.scanlineCounter = 0; |
| 151 | + |
| 152 | + // Clear the mock history |
| 153 | + mockTeletext.setRA0.mockClear(); |
| 154 | + |
| 155 | + // For non-interlaced mode, the RA0 value is just the lowest bit of scanlineCounter |
| 156 | + video.interlacedSyncAndVideo = false; |
| 157 | + |
| 158 | + // We need to set up the registers to allow endOfScanline to work |
| 159 | + video.regs[9] = 10; // Max scanline number that triggers endOfCharacterLine |
| 160 | + |
| 161 | + // Call endOfScanline to increment scanlineCounter to 1 |
| 162 | + video.endOfScanline(); |
| 163 | + |
| 164 | + // Verify scanlineCounter was incremented |
| 165 | + expect(video.scanlineCounter).toBe(1); |
| 166 | + |
| 167 | + // Verify setRA0 was called with the correct value (bit 0 is 1) |
| 168 | + expect(mockTeletext.setRA0).toHaveBeenCalledWith(true); |
| 169 | + |
| 170 | + // Clear the mock history |
| 171 | + mockTeletext.setRA0.mockClear(); |
| 172 | + |
| 173 | + // Call endOfScanline again to increment scanlineCounter to 2 |
| 174 | + video.endOfScanline(); |
| 175 | + |
| 176 | + // Verify scanlineCounter was incremented |
| 177 | + expect(video.scanlineCounter).toBe(2); |
| 178 | + |
| 179 | + // Verify setRA0 was called with the correct value (bit 0 is 0) |
| 180 | + expect(mockTeletext.setRA0).toHaveBeenCalledWith(false); |
| 181 | + }); |
| 182 | + |
| 183 | + it("should handle interlaced RA0 correctly", () => { |
| 184 | + // Set up for interlaced mode |
| 185 | + video.interlacedSyncAndVideo = true; |
| 186 | + video.scanlineCounter = 0; |
| 187 | + video.frameCount = 1; // Odd frame number |
| 188 | + |
| 189 | + // Initialize registers |
| 190 | + video.regs[9] = 10; // Max scanline number |
| 191 | + |
| 192 | + // Clear the mock history |
| 193 | + mockTeletext.setRA0.mockClear(); |
| 194 | + |
| 195 | + // Call endOfScanline |
| 196 | + video.endOfScanline(); |
| 197 | + |
| 198 | + // In interlaced mode with odd frame count, externalScanline is scanlineCounter + 1 |
| 199 | + // So even though scanlineCounter is now 2 (bit 0 = 0), externalScanline is 3 (bit 0 = 1) |
| 200 | + expect(mockTeletext.setRA0).toHaveBeenCalledWith(true); |
| 201 | + }); |
| 202 | + |
| 203 | + it("should call setDEW when vsync state changes", () => { |
| 204 | + // Setup necessary conditions for vsync |
| 205 | + video.regs[7] = 10; // Vertical sync position |
| 206 | + video.vertCounter = 10; |
| 207 | + video.inVSync = false; |
| 208 | + video.hadVSyncThisRow = false; |
| 209 | + video.horizCounter = 1; // Non-zero to avoid end-of-line logic |
| 210 | + |
| 211 | + // Clear mock history |
| 212 | + mockTeletext.setDEW.mockClear(); |
| 213 | + |
| 214 | + // Triggering vsync is complex, we need to set up more state |
| 215 | + // Calling polltime with the right conditions |
| 216 | + video.polltime(1); |
| 217 | + |
| 218 | + // Since we've set up the vertical counter to match R7, vsync should start |
| 219 | + expect(video.inVSync).toBe(true); |
| 220 | + |
| 221 | + // Verify setDEW was called with the correct parameter |
| 222 | + expect(mockTeletext.setDEW).toHaveBeenCalledWith(true); |
| 223 | + }); |
| 224 | + }); |
| 225 | + |
| 226 | + describe("Teletext rendering", () => { |
| 227 | + beforeEach(() => { |
| 228 | + // Set teletext mode |
| 229 | + video.ula.write(0, 2); |
| 230 | + |
| 231 | + // Set up all display flags to make rendering active |
| 232 | + video.dispEnabled = EVERYTHINGENABLED; |
| 233 | + |
| 234 | + // Set coords to visible area |
| 235 | + video.bitmapX = 100; |
| 236 | + video.bitmapY = 100; |
| 237 | + |
| 238 | + // Set test data for video memory |
| 239 | + mockCpu.videoRead.mockReturnValue(0x42); |
| 240 | + }); |
| 241 | + |
| 242 | + it("should call fetchData in teletext mode", () => { |
| 243 | + // Clear mock history |
| 244 | + mockTeletext.fetchData.mockClear(); |
| 245 | + |
| 246 | + // Set up horizCounter to avoid vsync logic |
| 247 | + video.horizCounter = 10; |
| 248 | + |
| 249 | + // Poll to trigger rendering |
| 250 | + video.polltime(1); |
| 251 | + |
| 252 | + // Verify fetchData was called with the correct parameter |
| 253 | + expect(mockTeletext.fetchData).toHaveBeenCalledWith(0x42); |
| 254 | + }); |
| 255 | + |
| 256 | + it("should call render in teletext mode", () => { |
| 257 | + // Clear mock history |
| 258 | + mockTeletext.render.mockClear(); |
| 259 | + |
| 260 | + // Set up horizCounter to avoid vsync logic |
| 261 | + video.horizCounter = 10; |
| 262 | + |
| 263 | + // Poll to trigger rendering |
| 264 | + video.polltime(1); |
| 265 | + |
| 266 | + // Verify render was called with the expected parameters |
| 267 | + expect(mockTeletext.render).toHaveBeenCalledWith(expect.any(Uint32Array), expect.any(Number)); |
| 268 | + }); |
| 269 | + |
| 270 | + it("should not render in non-teletext mode", () => { |
| 271 | + // Switch to non-teletext mode |
| 272 | + video.ula.write(0, 0); |
| 273 | + expect(video.teletextMode).toBe(false); |
| 274 | + |
| 275 | + // Clear mock history |
| 276 | + mockTeletext.render.mockClear(); |
| 277 | + |
| 278 | + // Set up horizCounter to avoid vsync logic |
| 279 | + video.horizCounter = 10; |
| 280 | + |
| 281 | + // Poll to trigger rendering |
| 282 | + video.polltime(1); |
| 283 | + |
| 284 | + // Verify render was not called |
| 285 | + expect(mockTeletext.render).not.toHaveBeenCalled(); |
| 286 | + }); |
| 287 | + }); |
| 288 | +}); |
0 commit comments