2026-06-05 11:18:08 +07:00
|
|
|
/**
|
|
|
|
|
* Chip-to-chip net identity — Phase 0 of the multi-chip digital bus track
|
|
|
|
|
* (see project/multichip-bus/ in the velxio-prod repo).
|
|
|
|
|
*
|
|
|
|
|
* THE PROBLEM (root cause A, 00-problem-analysis.md section 2): a digital net
|
|
|
|
|
* is keyed by ONE integer pin number in the per-board PinManager. A board pin
|
|
|
|
|
* (Uno D7 = 7) is net-symmetric — everyone on the net shares the number. But a
|
|
|
|
|
* chip-to-chip net is keyed per-endpoint by `syntheticChipPin(chipId, pinName)`,
|
|
|
|
|
* so the two chips on one wire resolve to two DIFFERENT keys and never share a
|
|
|
|
|
* net. Each chip writes into a key the other never reads.
|
|
|
|
|
*
|
|
|
|
|
* THE FIX: assign every electrically-connected net a single canonical id via
|
|
|
|
|
* union-find over the wire graph, and mint ONE shared `syntheticNetPin(netId)`
|
|
|
|
|
* for any net that has two or more chip endpoints and no board pin. Every
|
|
|
|
|
* endpoint on that net resolves to the same key, so a write on one chip is
|
|
|
|
|
* visible to another through the existing synchronous PinManager fan-out.
|
|
|
|
|
*
|
|
|
|
|
* SCOPE (D-006, never-clone boundary): this module ONLY decides the shared key
|
|
|
|
|
* for pure chip-to-chip nets. It returns null for:
|
|
|
|
|
* - nets with a board pin -> traceDetailed's rule 1 (board priority) handles it
|
|
|
|
|
* - nets with <2 chip endpoints -> traceDetailed's rules 2/3 (single-chip own
|
|
|
|
|
* synthetic) handle the chip-to-component case unchanged
|
|
|
|
|
* Board emulation never enters this path; the regression surface is the
|
|
|
|
|
* existing chip-to-component examples, gated behind the `chipbus` flag (D-007).
|
|
|
|
|
*/
|
|
|
|
|
import { UnionFind } from '../spice/unionFind';
|
|
|
|
|
import { isBoardComponent, boardPinToNumber } from '../../utils/boardPinMapping';
|
|
|
|
|
import { syntheticNetPin } from './syntheticPins';
|
|
|
|
|
|
|
|
|
|
// Structural view of the slice of simulator state this module needs. The real
|
|
|
|
|
// useSimulatorStore state is a superset, so it satisfies this shape directly —
|
|
|
|
|
// declaring it structurally keeps the module pure and unit-testable without
|
|
|
|
|
// pulling in React / the Zustand store.
|
|
|
|
|
interface NetEndpointRef {
|
|
|
|
|
componentId: string;
|
|
|
|
|
pinName: string;
|
|
|
|
|
}
|
|
|
|
|
interface WireLike {
|
|
|
|
|
start: NetEndpointRef;
|
|
|
|
|
end: NetEndpointRef;
|
|
|
|
|
}
|
|
|
|
|
interface ComponentLike {
|
|
|
|
|
id: string;
|
|
|
|
|
metadataId: string;
|
|
|
|
|
}
|
|
|
|
|
interface BoardLike {
|
|
|
|
|
id: string;
|
|
|
|
|
boardKind: string;
|
|
|
|
|
}
|
|
|
|
|
export interface ChipNetState {
|
|
|
|
|
wires: readonly WireLike[];
|
|
|
|
|
components: readonly ComponentLike[];
|
|
|
|
|
boards: readonly BoardLike[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Endpoint key = `${componentId}::${pinName}`. velxio chip ids
|
|
|
|
|
// (`custom_chip_<ts>_<rand>`) and chip.json pin names are identifier-like and
|
|
|
|
|
// never contain `::`, so the split back to (componentId, pinName) is exact.
|
|
|
|
|
const SEP = '::';
|
|
|
|
|
function epKey(componentId: string, pinName: string): string {
|
|
|
|
|
return `${componentId}${SEP}${pinName}`;
|
|
|
|
|
}
|
|
|
|
|
function parseEpKey(key: string): { componentId: string; pinName: string } {
|
|
|
|
|
const i = key.indexOf(SEP);
|
|
|
|
|
return { componentId: key.slice(0, i), pinName: key.slice(i + SEP.length) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface NetInfo {
|
|
|
|
|
/** Lexicographically-smallest endpoint key in the net — stable canonical id
|
|
|
|
|
* independent of union order, so the minted net pin number does not churn
|
|
|
|
|
* between resolve passes. */
|
|
|
|
|
canonical: string;
|
|
|
|
|
/** True if any endpoint on the net is a board pin that resolves to a real
|
|
|
|
|
* GPIO number (board priority defers to traceDetailed's rule 1). */
|
|
|
|
|
hasBoardPin: boolean;
|
|
|
|
|
/** Distinct custom-chip endpoint keys on the net. */
|
|
|
|
|
chipEndpoints: Set<string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ChipNetIndex {
|
|
|
|
|
/** Net representative for an endpoint key, or undefined if not on any wire. */
|
|
|
|
|
rootOf(key: string): string | undefined;
|
|
|
|
|
nets: Map<string, NetInfo>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Feature flag (D-007) ─────────────────────────────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// Off by default. Enable with `?chipbus=on` or
|
|
|
|
|
// `localStorage.velxio.chipbus = 'on'`, mirroring sim-mixedmode's `?mixedmode`.
|
|
|
|
|
// Guards every browser global so the module is safe under vitest/node.
|
|
|
|
|
|
|
|
|
|
let testOverride: boolean | null = null;
|
|
|
|
|
/** Test seam: force the flag on/off, or pass null to restore real detection. */
|
|
|
|
|
export function setChipBusEnabledForTest(v: boolean | null): void {
|
|
|
|
|
testOverride = v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function chipBusEnabled(): boolean {
|
|
|
|
|
if (testOverride !== null) return testOverride;
|
|
|
|
|
try {
|
|
|
|
|
if (typeof window !== 'undefined' && window.location) {
|
|
|
|
|
const q = new URLSearchParams(window.location.search).get('chipbus');
|
|
|
|
|
if (q === 'on' || q === '1' || q === 'true') return true;
|
|
|
|
|
if (q === 'off' || q === '0' || q === 'false') return false;
|
|
|
|
|
}
|
|
|
|
|
if (typeof localStorage !== 'undefined') {
|
|
|
|
|
const v = localStorage.getItem('velxio.chipbus');
|
|
|
|
|
if (v === 'on' || v === '1' || v === 'true') return true;
|
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
|
|
|
if (v === 'off' || v === '0' || v === 'false') return false;
|
2026-06-05 11:18:08 +07:00
|
|
|
}
|
|
|
|
|
} catch {
|
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
|
|
|
/* SecurityError on localStorage, missing globals in tests — fall through */
|
2026-06-05 11:18:08 +07:00
|
|
|
}
|
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
|
|
|
// On by default: chip-to-chip buses are a core capability now. Single-chip and
|
|
|
|
|
// board nets are unaffected (they never take this path), so the only thing
|
|
|
|
|
// this enables is multi-chip buses, which were simply broken before. Override
|
|
|
|
|
// with ?chipbus=off or localStorage.velxio.chipbus = 'off'.
|
|
|
|
|
return true;
|
2026-06-05 11:18:08 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Net index (memoized by wire/component fingerprint) ───────────────────────
|
|
|
|
|
|
|
|
|
|
let cache: { sig: string; index: ChipNetIndex } | null = null;
|
|
|
|
|
|
|
|
|
|
function fingerprint(state: ChipNetState): string {
|
|
|
|
|
const w = state.wires
|
|
|
|
|
.map(
|
|
|
|
|
(x) =>
|
|
|
|
|
`${x.start.componentId}${SEP}${x.start.pinName}|${x.end.componentId}${SEP}${x.end.pinName}`,
|
|
|
|
|
)
|
|
|
|
|
.join(',');
|
|
|
|
|
const c = state.components.map((x) => `${x.id}:${x.metadataId}`).join(',');
|
|
|
|
|
const b = state.boards.map((x) => `${x.id}:${x.boardKind}`).join(',');
|
|
|
|
|
return `${w}#${c}#${b}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIndex(state: ChipNetState): ChipNetIndex {
|
|
|
|
|
const uf = new UnionFind();
|
|
|
|
|
for (const wire of state.wires) {
|
|
|
|
|
const a = epKey(wire.start.componentId, wire.start.pinName);
|
|
|
|
|
const b = epKey(wire.end.componentId, wire.end.pinName);
|
|
|
|
|
uf.union(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const compById = new Map(state.components.map((c) => [c.id, c]));
|
|
|
|
|
const boardById = new Map(state.boards.map((b) => [b.id, b]));
|
|
|
|
|
const nets = new Map<string, NetInfo>();
|
|
|
|
|
|
|
|
|
|
for (const [key, root] of uf.entries()) {
|
|
|
|
|
let info = nets.get(root);
|
|
|
|
|
if (!info) {
|
|
|
|
|
info = { canonical: key, hasBoardPin: false, chipEndpoints: new Set() };
|
|
|
|
|
nets.set(root, info);
|
|
|
|
|
}
|
|
|
|
|
if (key < info.canonical) info.canonical = key;
|
|
|
|
|
|
|
|
|
|
const { componentId, pinName } = parseEpKey(key);
|
|
|
|
|
const board = boardById.get(componentId);
|
|
|
|
|
if (board || isBoardComponent(componentId)) {
|
|
|
|
|
const kind = board?.boardKind ?? componentId;
|
|
|
|
|
// A real numbered board pin (including -1 power/GND) means a board owns
|
|
|
|
|
// this net; defer to traceDetailed's board-priority rule.
|
|
|
|
|
if (boardPinToNumber(kind, pinName) !== null) info.hasBoardPin = true;
|
|
|
|
|
} else if (compById.get(componentId)?.metadataId === 'custom-chip') {
|
|
|
|
|
info.chipEndpoints.add(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rootOf: (k) => (uf.has(k) ? uf.find(k) : undefined),
|
|
|
|
|
nets,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getChipNetIndex(state: ChipNetState): ChipNetIndex {
|
|
|
|
|
const sig = fingerprint(state);
|
|
|
|
|
if (cache && cache.sig === sig) return cache.index;
|
|
|
|
|
const index = buildIndex(state);
|
|
|
|
|
cache = { sig, index };
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Test seam: drop the memoized index (the fingerprint already invalidates it
|
|
|
|
|
* on real input changes; this is only for deterministic unit tests). */
|
|
|
|
|
export function resetChipNetIndexForTest(): void {
|
|
|
|
|
cache = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Public resolver ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shared net-canonical key for a chip pin on a pure chip-to-chip net, or null
|
|
|
|
|
* when the legacy resolver rules should handle it (flag off; board on the net;
|
|
|
|
|
* fewer than two chip endpoints). When non-null, EVERY endpoint of the same net
|
|
|
|
|
* gets the identical key, so writes and reads land on one PinManager slot.
|
|
|
|
|
*/
|
|
|
|
|
export function resolveChipNetKey(
|
|
|
|
|
state: ChipNetState,
|
|
|
|
|
componentId: string,
|
|
|
|
|
pinName: string,
|
|
|
|
|
): number | null {
|
|
|
|
|
if (!chipBusEnabled()) return null;
|
|
|
|
|
const idx = getChipNetIndex(state);
|
|
|
|
|
const root = idx.rootOf(epKey(componentId, pinName));
|
|
|
|
|
if (root === undefined) return null;
|
|
|
|
|
const info = idx.nets.get(root);
|
|
|
|
|
if (!info || info.hasBoardPin || info.chipEndpoints.size < 2) return null;
|
|
|
|
|
return syntheticNetPin(info.canonical);
|
|
|
|
|
}
|