|
| 1 | + |
| 2 | +import { Z80, Z80State } from "../common/cpu/ZilogZ80"; |
| 3 | +import { BasicScanlineMachine } from "../common/devices"; |
| 4 | +import { KeyFlags, newAddressDecoder, padBytes, Keys, makeKeycodeMap, newKeyboardHandler, EmuHalt } from "../common/emu"; |
| 5 | +import { MasterAudio, AY38910_Audio, TssChannelAdapter } from "../common/audio"; |
| 6 | + |
| 7 | +// MCR-II constants (91490 CPU board) |
| 8 | +const MCR2_XTAL = 19968000; |
| 9 | +const MCR2_CPU_FREQ = MCR2_XTAL / 4; // ~4.992 MHz |
| 10 | +const MCR2_NUM_VISIBLE_SCANLINES = 480; |
| 11 | +const MCR2_NUM_TOTAL_SCANLINES = 525; |
| 12 | +const MCR2_CANVAS_WIDTH = 512; |
| 13 | +const MCR2_FPS = 30; |
| 14 | +const MCR2_CYCLES_PER_LINE = Math.floor(MCR2_CPU_FREQ / (MCR2_NUM_TOTAL_SCANLINES * MCR2_FPS)); |
| 15 | + |
| 16 | +const MCR2_TILE_COLS = 32; |
| 17 | +const MCR2_TILE_ROWS = 30; |
| 18 | +const MCR2_TILE_SIZE = 16; |
| 19 | + |
| 20 | +const INITIAL_WATCHDOG = 16; |
| 21 | + |
| 22 | +function pal3bit(v: number): number { |
| 23 | + v &= 7; |
| 24 | + return (v << 5) | (v << 2) | (v >> 1); |
| 25 | +} |
| 26 | + |
| 27 | +const MCR2_KEYCODE_MAP = makeKeycodeMap([ |
| 28 | + [Keys.SELECT, 0, 0x1], // Coin 1 |
| 29 | + [Keys.START, 0, 0x4], // 1P Start |
| 30 | + [Keys.VK_2, 0, 0x8], // 2P Start |
| 31 | + [Keys.UP, 1, 0x1], // P1 Up |
| 32 | + [Keys.DOWN, 1, 0x2], // P1 Down |
| 33 | + [Keys.LEFT, 1, 0x4], // P1 Left |
| 34 | + [Keys.RIGHT, 1, 0x8], // P1 Right |
| 35 | + [Keys.A, 1, 0x10], // P1 Button 1 |
| 36 | + [Keys.B, 1, 0x20], // P1 Button 2 |
| 37 | + [Keys.P2_UP, 2, 0x1], // P2 Up |
| 38 | + [Keys.P2_DOWN, 2, 0x2], // P2 Down |
| 39 | + [Keys.P2_LEFT, 2, 0x4], // P2 Left |
| 40 | + [Keys.P2_RIGHT, 2, 0x8], // P2 Right |
| 41 | + [Keys.P2_A, 2, 0x10], // P2 Button 1 |
| 42 | + [Keys.P2_B, 2, 0x20], // P2 Button 2 |
| 43 | +]); |
| 44 | + |
| 45 | +// ROM blob layout |
| 46 | +const ROM_PROGRAM_START = 0x0; |
| 47 | +const ROM_PROGRAM_SIZE = 0xC000; |
| 48 | +const ROM_BG_GFX_START = 0xC000; |
| 49 | +const ROM_BG_GFX_SIZE = 0x4000; // 16KB background tiles (2 bitplanes × 8KB) |
| 50 | +const ROM_SPR_GFX_START = 0x12000; |
| 51 | +const ROM_SPR_GFX_SIZE = 2 * 0x8000; // 32KB sprites (4 bitplanes × 8KB) |
| 52 | +const SSIO_ROM_SIZE = 0x4000; |
| 53 | +const ROM_TOTAL_SIZE = ROM_PROGRAM_SIZE + ROM_BG_GFX_SIZE + ROM_SPR_GFX_SIZE + SSIO_ROM_SIZE; |
| 54 | + |
| 55 | +export class MCR2Machine extends BasicScanlineMachine { |
| 56 | + |
| 57 | + cpuFrequency = MCR2_CPU_FREQ; |
| 58 | + canvasWidth = MCR2_CANVAS_WIDTH; |
| 59 | + numVisibleScanlines = MCR2_NUM_VISIBLE_SCANLINES; |
| 60 | + numTotalScanlines = MCR2_NUM_TOTAL_SCANLINES; |
| 61 | + cpuCyclesPerLine = MCR2_CYCLES_PER_LINE; |
| 62 | + defaultROMSize = ROM_TOTAL_SIZE; |
| 63 | + sampleRate = MCR2_FPS * MCR2_NUM_TOTAL_SCANLINES * 2; |
| 64 | + |
| 65 | + cpu = new Z80(); |
| 66 | + ram = new Uint8Array(0x800); // E000-E7FF (NVRAM) |
| 67 | + sprram = new Uint8Array(0x200); // E800-E9FF (sprite RAM) |
| 68 | + vram = new Uint8Array(0x800); // F000-F7FF (video RAM) |
| 69 | + palram = new Uint8Array(0x80); // F800-F87F (palette RAM) |
| 70 | + |
| 71 | + palette = new Uint32Array(64); |
| 72 | + |
| 73 | + interruptEnabled = false; |
| 74 | + watchdog_counter = INITIAL_WATCHDOG; |
| 75 | + ctcVector = 0; |
| 76 | + |
| 77 | + audioadapter: TssChannelAdapter; |
| 78 | + psg1: AY38910_Audio; |
| 79 | + psg2: AY38910_Audio; |
| 80 | + |
| 81 | + constructor() { |
| 82 | + super(); |
| 83 | + var audio = new MasterAudio(); |
| 84 | + this.psg1 = new AY38910_Audio(audio); |
| 85 | + this.psg2 = new AY38910_Audio(audio); |
| 86 | + this.audioadapter = new TssChannelAdapter( |
| 87 | + [this.psg1.psg, this.psg2.psg], 2, this.sampleRate |
| 88 | + ); |
| 89 | + |
| 90 | + this.connectCPUMemoryBus(this); |
| 91 | + this.connectCPUIOBus(this.newIOBus()); |
| 92 | + this.inputs.set([0, 0, 0, 0xff, 0xff]); // inputs + DIP switches |
| 93 | + this.handler = newKeyboardHandler(this.inputs, MCR2_KEYCODE_MAP); |
| 94 | + } |
| 95 | + |
| 96 | + // Main CPU memory read |
| 97 | + read = newAddressDecoder([ |
| 98 | + [0x0000, 0xBFFF, 0xFFFF, (a) => { return this.rom ? this.rom[a] : 0; }], |
| 99 | + [0xC000, 0xDFFF, 0x7FF, (a) => { return this.ram[a]; }], |
| 100 | + [0xE000, 0xE7FF, 0x1FF, (a) => { return this.sprram[a]; }], |
| 101 | + [0xF000, 0xF7FF, 0x7FF, (a) => { return this.vram[a]; }], |
| 102 | + [0xF800, 0xFFFF, 0x7F, (a) => { return this.palram[a]; }], |
| 103 | + ]); |
| 104 | + |
| 105 | + readConst(a: number): number | null { |
| 106 | + return this.read(a); |
| 107 | + } |
| 108 | + |
| 109 | + // Main CPU memory write |
| 110 | + write = newAddressDecoder([ |
| 111 | + [0xC000, 0xDFFF, 0x7FF, (a, v) => { this.ram[a] = v; }], |
| 112 | + [0xE000, 0xE7FF, 0x1FF, (a, v) => { this.sprram[a] = v; }], |
| 113 | + [0xF000, 0xF7FF, 0x7FF, (a, v) => { this.vram[a] = v; }], |
| 114 | + [0xF800, 0xFFFF, 0x7F, (a, v) => { |
| 115 | + this.palram[a] = v; |
| 116 | + this.updatePalette(a); |
| 117 | + }], |
| 118 | + ]); |
| 119 | + |
| 120 | + // I/O bus |
| 121 | + newIOBus() { |
| 122 | + return { |
| 123 | + read: (addr: number) => { |
| 124 | + addr &= 0xFF; |
| 125 | + if (addr <= 0x04) { |
| 126 | + return this.inputs[addr]; // SSIO input ports |
| 127 | + } |
| 128 | + if (addr >= 0x08 && addr <= 0x0F) { |
| 129 | + return this.inputs[3]; // DIP switches |
| 130 | + } |
| 131 | + if (addr >= 0xF0 && addr <= 0xF3) { |
| 132 | + return 0; // CTC read |
| 133 | + } |
| 134 | + return 0; |
| 135 | + }, |
| 136 | + write: (addr: number, val: number) => { |
| 137 | + addr &= 0xFF; |
| 138 | + if (addr == 0xE0) { |
| 139 | + this.watchdog_counter = INITIAL_WATCHDOG; |
| 140 | + } |
| 141 | + if (addr >= 0xF0 && addr <= 0xF3) { |
| 142 | + // Z80 CTC write |
| 143 | + // Track vector base for IM2 interrupts |
| 144 | + if ((addr & 3) == 0 && !(val & 1)) { |
| 145 | + // Channel 0 control word, not a vector |
| 146 | + } else if ((val & 1) == 0) { |
| 147 | + // Interrupt vector (even, bit 0 = 0) |
| 148 | + this.ctcVector = val & 0xF8; |
| 149 | + } |
| 150 | + } |
| 151 | + // SSIO sound output (ports 0x00-0x07) |
| 152 | + if (addr >= 0x00 && addr <= 0x07) { |
| 153 | + // Sound commands - ignored for now |
| 154 | + } |
| 155 | + } |
| 156 | + }; |
| 157 | + } |
| 158 | + |
| 159 | + // Palette: MCR 9-bit format |
| 160 | + // From MAME: R = ((offset&1)<<2) | (pal>>6), G = pal&7, B = (pal>>3)&7 |
| 161 | + // Each color entry = 2 bytes; only odd byte carries color data |
| 162 | + updatePalette(offset: number) { |
| 163 | + let i = offset >> 1; |
| 164 | + if (i >= 64) return; |
| 165 | + let pal = this.palram[i * 2 + 1]; // odd byte has color data |
| 166 | + let r = pal3bit((pal >> 6) & 3); // R low 2 bits from pal[7:6] |
| 167 | + let g = pal3bit(pal & 7); // G from pal[2:0] |
| 168 | + let b = pal3bit((pal >> 3) & 7); // B from pal[5:3] |
| 169 | + // R MSB comes from even byte bit 0 |
| 170 | + let r_msb = (this.palram[i * 2] & 1) << 2; |
| 171 | + r = pal3bit(r_msb | ((pal >> 6) & 3)); |
| 172 | + this.palette[i] = 0xFF000000 | (b << 16) | (g << 8) | r; |
| 173 | + } |
| 174 | + |
| 175 | + // Draw one scanline |
| 176 | + drawScanline() { |
| 177 | + let sl = this.scanline; |
| 178 | + if (sl >= MCR2_NUM_VISIBLE_SCANLINES) return; |
| 179 | + |
| 180 | + let pixofs = sl * MCR2_CANVAS_WIDTH; |
| 181 | + let tileRow = Math.floor(sl / MCR2_TILE_SIZE); |
| 182 | + let tileY = sl % MCR2_TILE_SIZE; |
| 183 | + |
| 184 | + // Draw background tiles |
| 185 | + if (tileRow < MCR2_TILE_ROWS) { |
| 186 | + for (let tileCol = 0; tileCol < MCR2_TILE_COLS; tileCol++) { |
| 187 | + let vramOfs = (tileRow * MCR2_TILE_COLS + tileCol) * 2; |
| 188 | + let byte0 = this.vram[vramOfs]; |
| 189 | + let byte1 = this.vram[vramOfs + 1]; |
| 190 | + |
| 191 | + // 91490 tile format |
| 192 | + let tileCode = byte0 | ((byte1 & 0x03) << 8); |
| 193 | + let tilePalette = (byte1 >> 4) & 0x03; |
| 194 | + let flipX = (byte1 & 0x04) != 0; |
| 195 | + let flipY = (byte1 & 0x08) != 0; |
| 196 | + |
| 197 | + let ty = flipY ? (15 - tileY) : tileY; |
| 198 | + let pixX = tileCol * MCR2_TILE_SIZE; |
| 199 | + this.drawTileLine(pixofs + pixX, tileCode, ty, tilePalette, flipX); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + // Draw sprites on this scanline |
| 204 | + this.drawSpriteScanline(sl, pixofs); |
| 205 | + } |
| 206 | + |
| 207 | + // Render one row of a 16×16 background tile (2 bitplanes) |
| 208 | + drawTileLine(outOfs: number, tileCode: number, row: number, palette: number, flipX: boolean) { |
| 209 | + let gfxBase = ROM_BG_GFX_START; |
| 210 | + let halfSize = ROM_BG_GFX_SIZE / 2; // 8KB per bitplane |
| 211 | + |
| 212 | + // Tile layout: 32 bytes per tile per bitplane |
| 213 | + // Left 8 pixels in bytes 0-15 (one byte per row), right 8 pixels in bytes 16-31 |
| 214 | + let tileOfs = tileCode * 32; |
| 215 | + let leftByteOfs = tileOfs + (row < 8 ? row : row); // rows 0-15 |
| 216 | + let rightByteOfs = tileOfs + 16 + (row < 8 ? row : row); |
| 217 | + |
| 218 | + let p0L = this.rom[gfxBase + leftByteOfs] || 0; |
| 219 | + let p1L = this.rom[gfxBase + halfSize + leftByteOfs] || 0; |
| 220 | + let p0R = this.rom[gfxBase + rightByteOfs] || 0; |
| 221 | + let p1R = this.rom[gfxBase + halfSize + rightByteOfs] || 0; |
| 222 | + |
| 223 | + let colorBase = palette * 4; |
| 224 | + |
| 225 | + // Draw 16 pixels (left 8 + right 8) |
| 226 | + for (let x = 0; x < 8; x++) { |
| 227 | + let bit = 7 - x; |
| 228 | + let srcX = flipX ? (15 - x) : x; |
| 229 | + let color = ((p0L >> bit) & 1) | (((p1L >> bit) & 1) << 1); |
| 230 | + this.pixels[outOfs + srcX] = this.palette[colorBase + color]; |
| 231 | + } |
| 232 | + for (let x = 0; x < 8; x++) { |
| 233 | + let bit = 7 - x; |
| 234 | + let srcX = flipX ? (7 - x) : (8 + x); |
| 235 | + let color = ((p0R >> bit) & 1) | (((p1R >> bit) & 1) << 1); |
| 236 | + this.pixels[outOfs + srcX] = this.palette[colorBase + color]; |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + // Render sprites intersecting a given scanline (91464 sprite board, 4bpp) |
| 241 | + drawSpriteScanline(scanline: number, pixofs: number) { |
| 242 | + let gfxBase = ROM_SPR_GFX_START; |
| 243 | + let planeSize = ROM_SPR_GFX_SIZE / 4; // 8KB per bitplane |
| 244 | + |
| 245 | + // Iterate sprites back-to-front (last sprite = highest priority) |
| 246 | + for (let sprNum = 31; sprNum >= 0; sprNum--) { |
| 247 | + let base = sprNum * 4; |
| 248 | + let sy = (this.sprram[base] - 2) & 0xFF; |
| 249 | + sy = 496 - sy; // Y is inverted |
| 250 | + |
| 251 | + let attrib = this.sprram[base + 1]; |
| 252 | + let code = this.sprram[base + 2] | ((attrib & 0x08) ? 0x100 : 0); |
| 253 | + let sx = (this.sprram[base + 3] - 3) & 0x1FF; |
| 254 | + |
| 255 | + let flipX = (attrib & 0x10) != 0; |
| 256 | + let flipY = (attrib & 0x20) != 0; |
| 257 | + let sprPalette = (attrib & 0x03); |
| 258 | + let colorBase = 16 + sprPalette * 16; // sprites use palette entries 16-63 |
| 259 | + |
| 260 | + // Check scanline intersection with 32-pixel tall sprite |
| 261 | + let relY = scanline - sy; |
| 262 | + if (relY < 0 || relY >= 32) continue; |
| 263 | + |
| 264 | + let row = flipY ? (31 - relY) : relY; |
| 265 | + |
| 266 | + // Sprite GFX layout: 4 bitplanes, each 8KB |
| 267 | + // Each sprite = 128 bytes per plane (32 rows × 4 columns × 1 byte) |
| 268 | + // x: 4 columns of 8 pixels = {col*32 + row, ...} |
| 269 | + let sprOfs = code * 128 + row; |
| 270 | + |
| 271 | + for (let col = 0; col < 4; col++) { |
| 272 | + let byteOfs = sprOfs + col * 32; |
| 273 | + let p0 = this.rom[gfxBase + byteOfs] || 0; |
| 274 | + let p1 = this.rom[gfxBase + planeSize + byteOfs] || 0; |
| 275 | + let p2 = this.rom[gfxBase + planeSize * 2 + byteOfs] || 0; |
| 276 | + let p3 = this.rom[gfxBase + planeSize * 3 + byteOfs] || 0; |
| 277 | + |
| 278 | + for (let x = 0; x < 8; x++) { |
| 279 | + let bit = 7 - x; |
| 280 | + let color = ((p0 >> bit) & 1) | |
| 281 | + (((p1 >> bit) & 1) << 1) | |
| 282 | + (((p2 >> bit) & 1) << 2) | |
| 283 | + (((p3 >> bit) & 1) << 3); |
| 284 | + if (color == 0) continue; // transparent |
| 285 | + |
| 286 | + let px: number; |
| 287 | + if (flipX) { |
| 288 | + px = sx + 31 - (col * 8 + x); |
| 289 | + } else { |
| 290 | + px = sx + col * 8 + x; |
| 291 | + } |
| 292 | + |
| 293 | + if (px >= 0 && px < MCR2_CANVAS_WIDTH) { |
| 294 | + this.pixels[pixofs + px] = this.palette[colorBase + color]; |
| 295 | + } |
| 296 | + } |
| 297 | + } |
| 298 | + } |
| 299 | + } |
| 300 | + |
| 301 | + startScanline() { |
| 302 | + this.audio && this.audioadapter && this.audioadapter.generate(this.audio); |
| 303 | + } |
| 304 | + |
| 305 | + advanceFrame(trap) { |
| 306 | + var steps = super.advanceFrame(trap); |
| 307 | + |
| 308 | + // Watchdog |
| 309 | + if (this.watchdog_counter-- <= 0) { |
| 310 | + throw new EmuHalt("WATCHDOG FIRED"); |
| 311 | + } |
| 312 | + |
| 313 | + // Generate IM2 interrupt at vblank via CTC channel 0 |
| 314 | + this.cpu.interrupt(this.ctcVector); |
| 315 | + |
| 316 | + return steps; |
| 317 | + } |
| 318 | + |
| 319 | + reset() { |
| 320 | + super.reset(); |
| 321 | + this.watchdog_counter = INITIAL_WATCHDOG; |
| 322 | + this.interruptEnabled = false; |
| 323 | + this.ctcVector = 0; |
| 324 | + this.psg1.reset(); |
| 325 | + this.psg2.reset(); |
| 326 | + } |
| 327 | + |
| 328 | + loadROM(data) { |
| 329 | + this.rom = padBytes(data, this.defaultROMSize); |
| 330 | + // Rebuild palette |
| 331 | + for (let i = 0; i < 64; i++) { |
| 332 | + this.updatePalette(i * 2); |
| 333 | + } |
| 334 | + } |
| 335 | + |
| 336 | + loadState(state) { |
| 337 | + super.loadState(state); |
| 338 | + this.sprram.set(state.sprram); |
| 339 | + this.vram.set(state.vram); |
| 340 | + this.palram.set(state.palram); |
| 341 | + this.watchdog_counter = state.wdc; |
| 342 | + this.interruptEnabled = state.ie; |
| 343 | + this.ctcVector = state.ctcv; |
| 344 | + for (let i = 0; i < 64; i++) { |
| 345 | + this.updatePalette(i * 2); |
| 346 | + } |
| 347 | + } |
| 348 | + |
| 349 | + saveState() { |
| 350 | + var state = super.saveState(); |
| 351 | + state['sprram'] = this.sprram.slice(0); |
| 352 | + state['vram'] = this.vram.slice(0); |
| 353 | + state['palram'] = this.palram.slice(0); |
| 354 | + state['wdc'] = this.watchdog_counter; |
| 355 | + state['ie'] = this.interruptEnabled; |
| 356 | + state['ctcv'] = this.ctcVector; |
| 357 | + return state; |
| 358 | + } |
| 359 | +} |
0 commit comments