2026-06-05 22:14:40 +07:00
|
|
|
/**
|
|
|
|
|
* Phase 3 — Galaksija video display chip smoke test (project/multichip-bus/).
|
|
|
|
|
* Verifies the snoop + CHRGEN render path mechanically: a write of 'R' to the
|
|
|
|
|
* video address 0x2802 makes the display chip render lit pixels into that cell's
|
|
|
|
|
* framebuffer region. Exact glyph fidelity is verified visually in the browser.
|
|
|
|
|
*/
|
|
|
|
|
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 { resetBusNets } from '../simulation/customChips/busNets';
|
|
|
|
|
|
|
|
|
|
const dispPath = fileURLToPath(new URL('./fixtures/chipbus/galaksija-display.wasm', import.meta.url));
|
|
|
|
|
const have = existsSync(dispPath);
|
|
|
|
|
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
|
|
|
|
|
|
|
|
|
|
describe.skipIf(!have)('chipbus Phase 3 — Galaksija display renders a character', () => {
|
|
|
|
|
beforeEach(() => resetBusNets());
|
|
|
|
|
afterEach(() => resetBusNets());
|
|
|
|
|
|
|
|
|
|
it('a write of "R" to 0x2802 lights pixels in that cell', async () => {
|
|
|
|
|
const pm = new PinManager();
|
|
|
|
|
// Explicit pin keys (isolated; no net resolution needed for this snoop test).
|
|
|
|
|
const aKey = (i: number) => 1 + i; // A0..A13 -> 1..14
|
|
|
|
|
const dKey = (i: number) => 21 + i; // D0..D7 -> 21..28
|
|
|
|
|
const WR = 30;
|
|
|
|
|
const wires = new Map<string, number>([
|
|
|
|
|
...range(14).map((i) => [`A${i}`, aKey(i)] as [string, number]),
|
|
|
|
|
...range(8).map((i) => [`D${i}`, dKey(i)] as [string, number]),
|
|
|
|
|
['WR', WR],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const disp = await ChipInstance.create({
|
|
|
|
|
wasm: new Uint8Array(readFileSync(dispPath)),
|
|
|
|
|
componentId: 'disp',
|
|
|
|
|
pinManager: pm,
|
|
|
|
|
wires,
|
|
|
|
|
display: { width: 256, height: 128 },
|
|
|
|
|
});
|
|
|
|
|
let fb: Uint8Array | null = null;
|
|
|
|
|
disp.onFramebufferUpdate((rgba) => { fb = rgba as Uint8Array; });
|
|
|
|
|
disp.start();
|
|
|
|
|
|
|
|
|
|
// Drive address 0x2802 (A1,A11,A13), data 'R'=0x52 (D1,D4,D6), then pulse WR.
|
|
|
|
|
const setAddr = (addr: number) => { for (let i = 0; i < 14; i++) pm.triggerPinChange(aKey(i), ((addr >> i) & 1) === 1); };
|
|
|
|
|
const setData = (d: number) => { for (let i = 0; i < 8; i++) pm.triggerPinChange(dKey(i), ((d >> i) & 1) === 1); };
|
|
|
|
|
setAddr(0x2802);
|
|
|
|
|
setData(0x52);
|
|
|
|
|
pm.triggerPinChange(WR, false);
|
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
|
|
|
pm.triggerPinChange(WR, true); // rising edge -> latch + render into the buffer
|
|
|
|
|
disp.tickTimers(40_000_000n); // fire the ~30 fps blit timer so it paints
|
2026-06-05 22:14:40 +07:00
|
|
|
|
|
|
|
|
expect(fb, 'framebuffer produced').not.toBeNull();
|
2026-06-05 22:21:17 +07:00
|
|
|
// A lit pixel is bright green (G channel high); background is dark green.
|
|
|
|
|
const lit = (x: number, y: number) => fb![(y * 256 + x) * 4 + 1] > 0x80;
|
|
|
|
|
// Cell (col 2, row 0) spans x 16..23, y 0..7. Count lit pixels there.
|
|
|
|
|
let litCount = 0;
|
|
|
|
|
for (let y = 0; y < 8; y++) for (let x = 16; x < 24; x++) if (lit(x, y)) litCount++;
|
|
|
|
|
expect(litCount, 'the R glyph lit some pixels in its cell').toBeGreaterThan(3);
|
2026-06-05 22:14:40 +07:00
|
|
|
// A blank cell elsewhere (col 0) stays dark.
|
|
|
|
|
let litBlank = 0;
|
2026-06-05 22:21:17 +07:00
|
|
|
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (lit(x, y)) litBlank++;
|
2026-06-05 22:14:40 +07:00
|
|
|
expect(litBlank, 'an unwritten cell stays blank').toBe(0);
|
|
|
|
|
|
|
|
|
|
disp.dispose();
|
|
|
|
|
});
|
|
|
|
|
});
|