Skip to content

Commit fc00254

Browse files
committed
mcr: initial emulator (not yet working)
1 parent b0af89e commit fc00254

4 files changed

Lines changed: 399 additions & 0 deletions

File tree

src/machine/mcr.ts

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
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+
}

src/platform/_index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function importPlatform(name: string) : Promise<any> {
1818
case "gameboy": return import("../platform/gb");
1919
case "kim1": return import("../platform/kim1");
2020
case "markdown": return import("../platform/markdown");
21+
case "mcr": return import("../platform/mcr");
2122
case "msx": return import("../platform/msx");
2223
case "mw8080bw": return import("../platform/mw8080bw");
2324
case "nes": return import("../platform/nes");

src/platform/mcr.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
import { MCR2Machine } from "../machine/mcr";
3+
import { BaseZ80MachinePlatform } from "../common/baseplatform";
4+
import { Platform } from "../common/baseplatform";
5+
import { PLATFORMS } from "../common/emu";
6+
7+
const MCR2_PRESETS = [
8+
{ id: 'minimal.c', name: 'Minimal Example' },
9+
{ id: 'gfxtest.c', name: 'Graphics Test' },
10+
];
11+
12+
class MCR2Platform extends BaseZ80MachinePlatform<MCR2Machine> implements Platform {
13+
14+
newMachine() { return new MCR2Machine(); }
15+
getPresets() { return MCR2_PRESETS; }
16+
readAddress(a) { return this.machine.readConst(a); }
17+
readVRAMAddress(a) {
18+
if (a < 0x200) return this.machine.sprram[a];
19+
if (a < 0xa00) return this.machine.vram[a - 0x200];
20+
return this.machine.palram[(a - 0xa00) & 0x7f];
21+
}
22+
getMemoryMap = function() { return { main:[
23+
{name:'Program ROM', start:0x0000, size:0xE000, type:'rom'},
24+
{name:'NVRAM', start:0xE000, size:0x800, type:'ram'},
25+
{name:'Sprite RAM', start:0xE800, size:0x200, type:'ram'},
26+
{name:'Video RAM', start:0xF000, size:0x800, type:'ram'},
27+
{name:'Palette RAM', start:0xF800, size:0x80, type:'ram'},
28+
] } };
29+
}
30+
31+
PLATFORMS['mcr'] = MCR2Platform;

0 commit comments

Comments
 (0)