2026-06-05 22:14:40 +07:00
|
|
|
/**
|
|
|
|
|
* Phase 3 — the COMPLETE Galaksija computer renders "READY" on screen
|
|
|
|
|
* (project/multichip-bus/). The full machine over the chip-to-chip bus:
|
|
|
|
|
* Z80 + galaksija-rom (0x0000-0x1FFF) + ram-64k (0x2000-0x3FFF) +
|
|
|
|
|
* inverter (address decode) + galaksija-display (snoops video RAM 0x2800).
|
|
|
|
|
*
|
|
|
|
|
* Boot the public-domain ROM; the monitor clears the 32x16 screen and writes
|
|
|
|
|
* its "READY" prompt to video RAM at 0x2802. The display chip snoops those bus
|
|
|
|
|
* writes and renders them with the CHRGEN font. We assert the on-screen "READY"
|
|
|
|
|
* cells (row 0, cols 2..6) light up — i.e. a real 1983 home computer boots AND
|
|
|
|
|
* draws its prompt, fully through the simulated bus.
|
|
|
|
|
*/
|
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
import { PinManager } from '../simulation/PinManager';
|
|
|
|
|
import { ChipInstance } from '../simulation/customChips/ChipRuntime';
|
|
|
|
|
import {
|
|
|
|
|
resolveChipNetKey, setChipBusEnabledForTest, resetChipNetIndexForTest, type ChipNetState,
|
|
|
|
|
} from '../simulation/customChips/chipNets';
|
|
|
|
|
import { syntheticChipPin } from '../simulation/customChips/syntheticPins';
|
|
|
|
|
import { resetBusNets } from '../simulation/customChips/busNets';
|
|
|
|
|
|
|
|
|
|
const f = (n: string) => fileURLToPath(new URL(`./fixtures/chipbus/${n}`, import.meta.url));
|
|
|
|
|
const paths = { z80: f('z80.wasm'), rom: f('galaksija-rom.wasm'), ram: f('ram-64k.wasm'), inv: f('inverter.wasm'), disp: f('galaksija-display.wasm') };
|
|
|
|
|
const have = Object.values(paths).every(existsSync);
|
|
|
|
|
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
|
|
|
|
|
|
|
|
|
|
const Z80_PINS = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`),
|
|
|
|
|
'M1', 'MREQ', 'IORQ', 'RD', 'WR', 'RFSH', 'HALT', 'WAIT', 'INT', 'NMI', 'RESET', 'BUSREQ', 'BUSACK', 'CLK', 'VCC', 'GND'];
|
|
|
|
|
const ROM_PINS = [...range(13).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE'];
|
|
|
|
|
const RAM_PINS = [...range(16).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'CE', 'OE', 'WE', 'VCC', 'GND'];
|
|
|
|
|
const INV_PINS = ['IN', 'OUT'];
|
|
|
|
|
const DISP_PINS = [...range(14).map((i) => `A${i}`), ...range(8).map((i) => `D${i}`), 'WR'];
|
|
|
|
|
|
|
|
|
|
const W: ChipNetState['wires'] = [];
|
|
|
|
|
const wire = (a: string, ap: string, b: string, bp: string) =>
|
|
|
|
|
(W as { start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }[]).push(
|
|
|
|
|
{ start: { componentId: a, pinName: ap }, end: { componentId: b, pinName: bp } });
|
|
|
|
|
for (const i of range(13)) { wire('z80', `A${i}`, 'rom', `A${i}`); wire('z80', `A${i}`, 'ram', `A${i}`); wire('z80', `A${i}`, 'disp', `A${i}`); }
|
|
|
|
|
wire('z80', 'A13', 'rom', 'CE'); wire('z80', 'A13', 'inv', 'IN'); wire('z80', 'A13', 'disp', 'A13');
|
|
|
|
|
wire('inv', 'OUT', 'ram', 'CE');
|
|
|
|
|
for (const i of range(8)) { wire('z80', `D${i}`, 'rom', `D${i}`); wire('z80', `D${i}`, 'ram', `D${i}`); wire('z80', `D${i}`, 'disp', `D${i}`); }
|
|
|
|
|
wire('z80', 'RD', 'rom', 'OE'); wire('z80', 'RD', 'ram', 'OE');
|
|
|
|
|
wire('z80', 'WR', 'ram', 'WE'); wire('z80', 'WR', 'disp', 'WR');
|
|
|
|
|
|
|
|
|
|
const STATE: ChipNetState = {
|
|
|
|
|
wires: W,
|
|
|
|
|
components: ['z80', 'rom', 'ram', 'inv', 'disp'].map((id) => ({ id, metadataId: 'custom-chip' })),
|
|
|
|
|
boards: [],
|
|
|
|
|
};
|
|
|
|
|
const pinKey = (c: string, p: string): number => resolveChipNetKey(STATE, c, p) ?? syntheticChipPin(c, p);
|
|
|
|
|
const wiresFor = (c: string, pins: string[]) => new Map(pins.map((p) => [p, pinKey(c, p)] as [string, number]));
|
|
|
|
|
|
|
|
|
|
describe.skipIf(!have)('chipbus Phase 3 — full Galaksija computer renders READY', () => {
|
|
|
|
|
beforeEach(() => { setChipBusEnabledForTest(true); resetChipNetIndexForTest(); resetBusNets(); });
|
|
|
|
|
afterEach(() => { setChipBusEnabledForTest(null); resetChipNetIndexForTest(); resetBusNets(); });
|
|
|
|
|
|
|
|
|
|
it('boots the ROM and renders the READY prompt on the video display', async () => {
|
|
|
|
|
const pm = new PinManager();
|
|
|
|
|
const mk = async (k: keyof typeof paths, id: string, pins: string[], display?: { width: number; height: number }) =>
|
|
|
|
|
ChipInstance.create({ wasm: new Uint8Array(readFileSync(paths[k])), componentId: id, pinManager: pm, wires: wiresFor(id, pins), display });
|
|
|
|
|
const z80 = await mk('z80', 'z80', Z80_PINS); z80.start();
|
|
|
|
|
const rom = await mk('rom', 'rom', ROM_PINS); rom.start();
|
|
|
|
|
const ram = await mk('ram', 'ram', RAM_PINS); ram.start();
|
|
|
|
|
const inv = await mk('inv', 'inv', INV_PINS); inv.start();
|
|
|
|
|
const disp = await mk('disp', 'disp', DISP_PINS, { width: 256, height: 128 });
|
|
|
|
|
let fb: Uint8Array | null = null;
|
|
|
|
|
disp.onFramebufferUpdate((rgba) => { fb = rgba as Uint8Array; });
|
|
|
|
|
disp.start();
|
|
|
|
|
|
|
|
|
|
for (const p of ['WAIT', 'INT', 'NMI', 'BUSREQ']) pm.triggerPinChange(pinKey('z80', p), true);
|
|
|
|
|
pm.triggerPinChange(pinKey('z80', 'RESET'), false);
|
|
|
|
|
pm.triggerPinChange(pinKey('z80', 'RESET'), true);
|
|
|
|
|
|
|
|
|
|
z80.tickTimers(BigInt(120000 * 250)); // enough for clear + banner
|
feat(chipbus): Galaksija home computer gallery example + browser perf throttle
Ships the full Galaksija (1983 Z80 home computer) as a runnable Retro
gallery example, plus the pieces needed to run a multi-chip bus live in the
browser.
Gallery example (examples-retro-intel.ts, id 'galaksija-z80-computer'):
Z80 + galaksija-rom (public-domain ROM A+B) + ram-64k + inverter (A13
decode) + galaksija-display + a power-on reset chip, wired chip-to-chip
over the bus (76 wires), no board. Click Resume and it boots the real ROM
to the "READY" prompt on the green display. Chip wasm is embedded
(wasmBase64) so it runs without a backend compile.
- ChipRuntime.tickTimers gains a wall-clock budget (CustomChipPart passes
6 ms): a faithful-but-slow event-driven bus can't run a real-time CPU in
one animation frame, so without a cap a Z80 fetching over the settle
kernel froze the tab. With the budget the sim advances slower than real
time (boots over a few seconds) and the UI stays responsive; fast
single-chip examples finish under budget and are unaffected.
- galaksija-display: blits its framebuffer on a ~30 fps timer instead of on
every character write, so a clear-screen burst doesn't flood the canvas.
- reset-gen: power-on reset (pulses RESET high, ties WAIT/BUSREQ/INT/NMI
high) so the machine boots on Resume without a manual reset.
- chipbus flag now defaults ON (override with ?chipbus=off): chip-to-chip
buses are a core capability; single-chip and board nets never take this
path, so the only thing enabled is multi-chip buses, previously broken.
Verified live in the browser: the example boots and renders "@'READY" with
the ">_" prompt, responsive. Full suite 2084 pass (5 pre-existing,
unrelated env failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:49:33 +07:00
|
|
|
disp.tickTimers(50_000_000n); // fire the display's ~30 fps blit timer to paint
|
2026-06-05 22:14:40 +07:00
|
|
|
|
|
|
|
|
expect(fb, 'display produced a framebuffer').not.toBeNull();
|
|
|
|
|
// "READY" lives at video offset 2 (0x2802) -> row 0, cols 2..6. Count lit
|
2026-06-05 22:21:17 +07:00
|
|
|
// pixels (bright-green G channel) across those five character cells.
|
2026-06-05 22:14:40 +07:00
|
|
|
let litReady = 0;
|
2026-06-05 22:21:17 +07:00
|
|
|
for (let y = 0; y < 8; y++) for (let x = 16; x < 56; x++) if (fb![(y * 256 + x) * 4 + 1] > 0x80) litReady++;
|
2026-06-05 22:14:40 +07:00
|
|
|
expect(litReady, 'the READY prompt is rendered on screen').toBeGreaterThan(20);
|
|
|
|
|
|
|
|
|
|
z80.dispose(); rom.dispose(); ram.dispose(); inv.dispose(); disp.dispose();
|
|
|
|
|
}, 30_000);
|
|
|
|
|
});
|