From e3d21cd6fc738f7bc446076039eac80814aeac78 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 5 Jun 2026 12:14:40 -0300 Subject: [PATCH] feat(chipbus): Galaksija video display chip - full computer renders READY galaksija-display.c: a 32x16 text video chip that renders the Galaksija video RAM. It is a passive bus snoop -- watches WR + address + data, and on a write into the 0x2800 video region stores the character and renders that cell into a 256x128 framebuffer using the public-domain CHRGEN font (code*8, bit 0 = lit). It never drives the bus. The host blits the framebuffer to the chip canvas (vx_framebuffer_init / vx_buffer_write). Two tests: - chipbus-galaksija-display: snoop+render smoke test (a write of 'R' to 0x2802 lights its cell; unwritten cells stay blank). - chipbus-galaksija-computer: the COMPLETE machine over the chip-to-chip bus (Z80 + galaksija-rom + ram-64k + inverter decode + galaksija-display) boots the public-domain ROM and renders the monitor's "READY" prompt on screen. 40 chipbus tests across 10 files pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../chipbus-galaksija-computer.test.ts | 87 ++++++++ .../chipbus-galaksija-display.test.ts | 70 ++++++ .../fixtures/chipbus/galaksija-display.wasm | Bin 0 -> 23469 bytes .../sdk/examples/galaksija-display.c | 202 ++++++++++++++++++ .../sdk/examples/galaksija-display.chip.json | 9 + 5 files changed, 368 insertions(+) create mode 100644 frontend/src/__tests__/chipbus-galaksija-computer.test.ts create mode 100644 frontend/src/__tests__/chipbus-galaksija-display.test.ts create mode 100644 frontend/src/__tests__/fixtures/chipbus/galaksija-display.wasm create mode 100644 test/test_custom_chips/sdk/examples/galaksija-display.c create mode 100644 test/test_custom_chips/sdk/examples/galaksija-display.chip.json diff --git a/frontend/src/__tests__/chipbus-galaksija-computer.test.ts b/frontend/src/__tests__/chipbus-galaksija-computer.test.ts new file mode 100644 index 00000000..8ac4fdf5 --- /dev/null +++ b/frontend/src/__tests__/chipbus-galaksija-computer.test.ts @@ -0,0 +1,87 @@ +/** + * 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 + + expect(fb, 'display produced a framebuffer').not.toBeNull(); + // "READY" lives at video offset 2 (0x2802) -> row 0, cols 2..6. Count lit + // pixels across those five character cells (x 16..55, y 0..7). + let litReady = 0; + for (let y = 0; y < 8; y++) for (let x = 16; x < 56; x++) if (fb![(y * 256 + x) * 4] === 0xff) litReady++; + expect(litReady, 'the READY prompt is rendered on screen').toBeGreaterThan(20); + + z80.dispose(); rom.dispose(); ram.dispose(); inv.dispose(); disp.dispose(); + }, 30_000); +}); diff --git a/frontend/src/__tests__/chipbus-galaksija-display.test.ts b/frontend/src/__tests__/chipbus-galaksija-display.test.ts new file mode 100644 index 00000000..86833c24 --- /dev/null +++ b/frontend/src/__tests__/chipbus-galaksija-display.test.ts @@ -0,0 +1,70 @@ +/** + * 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([ + ...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); + pm.triggerPinChange(WR, true); // rising edge -> latch + render + + expect(fb, 'framebuffer produced').not.toBeNull(); + // Cell (col 2, row 0) spans x 16..23, y 0..7. Count lit (white) pixels there. + let lit = 0; + for (let y = 0; y < 8; y++) { + for (let x = 16; x < 24; x++) { + const o = (y * 256 + x) * 4; + if (fb![o] === 0xff) lit++; + } + } + expect(lit, 'the R glyph lit some pixels in its cell').toBeGreaterThan(3); + // A blank cell elsewhere (col 0) stays dark. + let litBlank = 0; + for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (fb![(y * 256 + x) * 4] === 0xff) litBlank++; + expect(litBlank, 'an unwritten cell stays blank').toBe(0); + + disp.dispose(); + }); +}); diff --git a/frontend/src/__tests__/fixtures/chipbus/galaksija-display.wasm b/frontend/src/__tests__/fixtures/chipbus/galaksija-display.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b30200657ae13bde57a899c288411bdd841b8c27 GIT binary patch literal 23469 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 literal 0 HcmV?d00001 diff --git a/test/test_custom_chips/sdk/examples/galaksija-display.c b/test/test_custom_chips/sdk/examples/galaksija-display.c new file mode 100644 index 00000000..9855bebf --- /dev/null +++ b/test/test_custom_chips/sdk/examples/galaksija-display.c @@ -0,0 +1,202 @@ +/* + * galaksija-display — Galaksija (1983) 32x16 text video, rendered from its + * memory-mapped video RAM. Phase 3 of the multi-chip bus track + * (project/multichip-bus/). Public-domain character generator (CHRGEN) by the + * Galaksija project. The chip is a passive BUS SNOOP: it watches WR + the + * address + data bus and, on a write into the 0x2800-0x2BFF video region, + * stores the character and renders that cell into its framebuffer using CHRGEN. + * It never drives the bus. The framebuffer (256x128 = 32x16 chars of 8x8) is + * blitted to the chip canvas by the host (vx_framebuffer_init / vx_buffer_write). + * + * CHRGEN: 256 chars x 8 rows, 1 byte per row, bit = 0 -> lit pixel (inverted). + */ +#include "velxio-chip.h" +#include +#include + +#define COLS 32 +#define ROWS 16 +#define CW 8 +#define CH 8 +#define FB_W (COLS*CW) +#define FB_H (ROWS*CH) + +static const uint8_t chrgen[2048] = { + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xaf,0xbf,0xaf,0xaf,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0xdf,0xdf,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0x03,0xcf,0x83,0x87,0x83,0x03,0x03,0x87,0x7b,0xc7,0x03,0x7b,0xfb,0x7b,0x7b,0x87, + 0x83,0x87,0x83,0x87,0x03,0x7b,0x7b,0x7b,0x7b,0x7b,0x03,0x87,0x87,0x03,0x87,0xff, + 0xff,0xef,0xd7,0xb7,0xdf,0xff,0xe7,0xf8,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xbf, + 0x87,0xdf,0x87,0x87,0xbf,0x03,0x87,0x03,0x87,0x87,0xff,0xff,0xff,0xff,0xff,0x87, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11,0xff,0xf1,0x1f,0x11, + 0xfb,0xb7,0x77,0x7b,0x77,0xfb,0xfb,0x7b,0x7b,0xef,0x7f,0xbb,0xfb,0x33,0x7b,0x7b, + 0x7b,0x7b,0x7b,0x7b,0xef,0x7b,0x7b,0x7b,0x7b,0x7b,0x7f,0x7b,0x7b,0x7f,0x7b,0xff, + 0xff,0xef,0xd7,0xb7,0x07,0x73,0xdb,0xfb,0xef,0xdf,0xef,0xef,0xff,0xff,0xff,0xbf, + 0x7b,0xcf,0x7b,0x7b,0x9f,0xfb,0x7b,0x7b,0x7b,0x7b,0xe7,0xe7,0xbf,0xff,0xf7,0x7b, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xbb,0x7b,0x77,0xfb,0x77,0xfb,0xfb,0xfb,0x7b,0xef,0x7f,0xdb,0xfb,0x33,0x7b,0x7b, + 0x7b,0x7b,0x7b,0xfb,0xef,0x7b,0x7b,0x7b,0x7b,0x7b,0xbf,0xfb,0xfb,0xbf,0xfb,0xff, + 0xff,0xef,0xd7,0xb7,0xdb,0x73,0xdb,0xfb,0xf7,0xbf,0xab,0xef,0xff,0xff,0xff,0xdf, + 0x7b,0xdf,0x7f,0x7f,0xaf,0xfb,0xfb,0x7f,0x7b,0x7b,0xe7,0xe7,0xdf,0xff,0xef,0x7b, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xfb,0x7b,0x77,0xfb,0x77,0xfb,0xfb,0xfb,0x7b,0xef,0x7f,0xeb,0xfb,0x4b,0x7b,0x7b, + 0x7b,0x7b,0x7b,0xfb,0xef,0x7b,0xb7,0x7b,0xb7,0x7b,0xdf,0xfb,0xfb,0xdf,0xfb,0xff, + 0xff,0xef,0xff,0x03,0xdb,0xbf,0xeb,0xfb,0xf7,0xbf,0xc7,0xef,0xff,0xff,0xff,0xdf, + 0x3b,0xdf,0x7f,0x7f,0xb7,0x83,0x83,0xbf,0x7b,0x7b,0xff,0xff,0xef,0x03,0xdf,0x7f, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xeb,0x7b,0x87,0xfb,0x77,0x83,0x83,0x1b,0x03,0xef,0x7f,0xf3,0xfb,0x4b,0x73,0x7b, + 0x83,0x7b,0x83,0x87,0xef,0x7b,0xb7,0x7b,0xcf,0x07,0xef,0xfb,0xfb,0xef,0x87,0xff, + 0xff,0xef,0xff,0xb7,0x87,0xdf,0xf7,0xfb,0xf7,0xbf,0x83,0x83,0xff,0x03,0xff,0xef, + 0x5b,0xdf,0xbf,0x9f,0xbb,0x7f,0x7b,0xdf,0x87,0x7b,0xff,0xff,0xf7,0xff,0xbf,0x9f, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xfb,0x03,0x77,0xfb,0x77,0xfb,0xfb,0x7b,0x7b,0xef,0x7f,0xeb,0xfb,0x7b,0x6b,0x7b, + 0xfb,0x7b,0xeb,0x7f,0xef,0x7b,0xb7,0x4b,0xcf,0x7f,0xf7,0xfb,0xfb,0xf7,0x7f,0xff, + 0xff,0xef,0xff,0xb7,0x6f,0xef,0xeb,0xfb,0xf7,0xbf,0xc7,0xef,0xff,0xff,0xff,0xef, + 0x6b,0xdf,0xc7,0x7f,0xbb,0x7f,0x7b,0xef,0x7b,0x07,0xff,0xff,0xef,0x03,0xdf,0xef, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xf1,0xf1,0xf1,0xf1,0x1f,0x1f,0x1f,0x1f,0x11,0x11,0x11,0x11, + 0xab,0x7b,0x77,0xfb,0x77,0xfb,0xfb,0x7b,0x7b,0xef,0x7b,0xdb,0xfb,0x7b,0x5b,0x7b, + 0xfb,0x5b,0xdb,0x7f,0xef,0x7b,0xcf,0x4b,0xb7,0x7f,0xfb,0xfb,0xfb,0xfb,0x7f,0xff, + 0xff,0xef,0xff,0x03,0x6f,0xf7,0x5b,0xfa,0xf7,0xbf,0xab,0xef,0xe7,0xff,0xff,0xf7, + 0x73,0xdf,0xfb,0x7f,0x03,0x7f,0x7b,0xf7,0x7b,0x7f,0xe7,0xe7,0xdf,0xff,0xef,0xef, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xfb,0x7b,0x77,0x7b,0x77,0xfb,0xfb,0x7b,0x7b,0xef,0x7b,0xbb,0xfb,0x7b,0x3b,0x7b, + 0xfb,0xbb,0xbb,0x7b,0xef,0x7b,0xcf,0x33,0x7b,0x7b,0xfb,0x7b,0x7b,0xfb,0x7b,0xff, + 0xff,0xff,0xff,0xb7,0x83,0x3b,0xbb,0xfb,0xef,0xdf,0xef,0xef,0xe7,0xff,0xe7,0xf7, + 0x7b,0xdf,0xfb,0x7b,0xbf,0x7b,0x7b,0xf7,0x7b,0x7b,0xe7,0xe7,0xbf,0xff,0xf7,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1, + 0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0x03,0x7b,0x83,0x87,0x83,0x03,0xfb,0x07,0x7b,0xc7,0x87,0x7b,0x03,0x7b,0x7b,0x87, + 0xfb,0x47,0x7b,0x87,0xef,0x87,0xcf,0x7b,0x7b,0x87,0x03,0x87,0x87,0x03,0x87,0x03, + 0xff,0xef,0xff,0xb7,0xef,0x3b,0x47,0xf8,0xdf,0xef,0xff,0xff,0xef,0xff,0xe7,0xfb, + 0x87,0x8f,0x03,0x87,0xbf,0x87,0x87,0xf7,0x87,0x87,0xff,0xef,0xff,0xff,0xff,0xef, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1, + 0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xfb, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1, + 0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1,0xf1, + 0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, +}; + +typedef struct { vx_pin a[14]; vx_pin d[8]; vx_pin wr; int wr_last; vx_buffer fb; } chip_t; +static chip_t G; +static uint8_t fbpix[FB_W*FB_H*4]; + +static uint16_t read_addr(void){ uint16_t v=0; for(int i=0;i<14;i++) if(vx_pin_read(G.a[i])) v|=(1u<=FB_W||y>=FB_H) return; + int o=(y*FB_W+x)*4; uint8_t v=lit?0xFF:0x00; + fbpix[o]=v; fbpix[o+1]=v; fbpix[o+2]=v; fbpix[o+3]=0xFF; +} +static void render_cell(int col,int row,uint8_t ch){ + for(int line=0; line>(7-px))&1) ? 0 : 1; /* 0 bit = lit */ + put_px(col*CW+px, row*CH+line, lit); + } + } + vx_buffer_write(G.fb, 0, fbpix, sizeof(fbpix)); +} + +static void on_wr(void* u, vx_pin p, int value){ + (void)u;(void)p; + /* latch on WR rising edge (data + address valid through the write) */ + if(G.wr_last==0 && value==1){ + uint16_t addr=read_addr(); + /* video region 0x2800-0x2BFF: A13=1,A12=0,A11=1,A10=0 */ + if((addr & 0x3C00) == 0x2800){ + uint16_t off = addr & 0x3FF; /* 0..1023 */ + int row=off/COLS, col=off%COLS; + if(row