2026-04-29 05:24:39 +07:00
|
|
|
/**
|
|
|
|
|
* ChipRuntime — TypeScript port of test/test_custom_chips/src/ChipRuntime.js.
|
|
|
|
|
*
|
|
|
|
|
* Loads a Velxio custom-chip WASM, wires its imports to host services
|
|
|
|
|
* (PinManager, I2CBusManager, SPIBus, attribute storage, timer queue), and
|
|
|
|
|
* dispatches its callbacks back into the simulator. One ChipInstance per
|
|
|
|
|
* chip dropped on the canvas.
|
|
|
|
|
*/
|
|
|
|
|
import type { PinManager } from '../PinManager';
|
|
|
|
|
import type { I2CBusManager } from '../I2CBusManager';
|
|
|
|
|
import { SPIBus, SPIDevice } from './SPIBus';
|
|
|
|
|
import { WasiShim, type SimNanosFn, type WriteStdoutFn } from './WasiShim';
|
2026-06-03 10:40:49 +07:00
|
|
|
import { setChipPinDrive } from './chipPinDrives';
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
import { isSyntheticChipPin, isSyntheticNetPin } from './syntheticPins';
|
2026-06-03 10:40:49 +07:00
|
|
|
import { requestElectricalResolve } from '../spice/electricalResolveHook';
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
import { chipBusEnabled } from './chipNets';
|
|
|
|
|
import { setBusDrive, clearBusDriversForChip } from './busNets';
|
|
|
|
|
import { modeToDrive } from './busLogic';
|
2026-04-29 05:24:39 +07:00
|
|
|
|
|
|
|
|
function readCString(memory: WebAssembly.Memory, ptr: number): string {
|
|
|
|
|
const u8 = new Uint8Array(memory.buffer);
|
|
|
|
|
let end = ptr;
|
|
|
|
|
while (end < u8.length && u8[end] !== 0) end++;
|
|
|
|
|
return new TextDecoder().decode(u8.subarray(ptr, end));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface I2CConfig {
|
|
|
|
|
address: number;
|
|
|
|
|
scl: number;
|
|
|
|
|
sda: number;
|
|
|
|
|
on_connect: number;
|
|
|
|
|
on_read: number;
|
|
|
|
|
on_write: number;
|
|
|
|
|
on_stop: number;
|
|
|
|
|
user_data: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface UartConfig {
|
|
|
|
|
rx: number;
|
|
|
|
|
tx: number;
|
|
|
|
|
baud_rate: number;
|
|
|
|
|
on_rx_byte: number;
|
|
|
|
|
on_tx_done: number;
|
|
|
|
|
user_data: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SpiConfig {
|
|
|
|
|
sck: number;
|
|
|
|
|
mosi: number;
|
|
|
|
|
miso: number;
|
|
|
|
|
cs: number;
|
|
|
|
|
mode: number;
|
|
|
|
|
on_done: number;
|
|
|
|
|
user_data: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readI2CConfig(memory: WebAssembly.Memory, ptr: number): I2CConfig {
|
|
|
|
|
const dv = new DataView(memory.buffer);
|
|
|
|
|
return {
|
|
|
|
|
address: dv.getUint8(ptr + 0),
|
|
|
|
|
scl: dv.getInt32(ptr + 4, true),
|
|
|
|
|
sda: dv.getInt32(ptr + 8, true),
|
|
|
|
|
on_connect: dv.getUint32(ptr + 12, true),
|
|
|
|
|
on_read: dv.getUint32(ptr + 16, true),
|
|
|
|
|
on_write: dv.getUint32(ptr + 20, true),
|
|
|
|
|
on_stop: dv.getUint32(ptr + 24, true),
|
|
|
|
|
user_data: dv.getUint32(ptr + 28, true),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readUartConfig(memory: WebAssembly.Memory, ptr: number): UartConfig {
|
|
|
|
|
const dv = new DataView(memory.buffer);
|
|
|
|
|
return {
|
|
|
|
|
rx: dv.getInt32(ptr + 0, true),
|
|
|
|
|
tx: dv.getInt32(ptr + 4, true),
|
|
|
|
|
baud_rate: dv.getUint32(ptr + 8, true),
|
|
|
|
|
on_rx_byte: dv.getUint32(ptr + 12, true),
|
|
|
|
|
on_tx_done: dv.getUint32(ptr + 16, true),
|
|
|
|
|
user_data: dv.getUint32(ptr + 20, true),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readSpiConfig(memory: WebAssembly.Memory, ptr: number): SpiConfig {
|
|
|
|
|
const dv = new DataView(memory.buffer);
|
|
|
|
|
return {
|
|
|
|
|
sck: dv.getInt32(ptr + 0, true),
|
|
|
|
|
mosi: dv.getInt32(ptr + 4, true),
|
|
|
|
|
miso: dv.getInt32(ptr + 8, true),
|
|
|
|
|
cs: dv.getInt32(ptr + 12, true),
|
|
|
|
|
mode: dv.getUint32(ptr + 16, true),
|
|
|
|
|
on_done: dv.getUint32(ptr + 20, true),
|
|
|
|
|
user_data: dv.getUint32(ptr + 24, true),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PinEntry {
|
|
|
|
|
name: string;
|
|
|
|
|
mode: number;
|
|
|
|
|
arduinoPin: number | null;
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
/** Last level written/initialized — used to compute the bus drive on a mode
|
|
|
|
|
* flip (e.g. OUTPUT -> INPUT releases the bus without forgetting the level). */
|
|
|
|
|
value: 0 | 1;
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AttrEntry {
|
|
|
|
|
name: string;
|
|
|
|
|
default: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TimerEntry {
|
|
|
|
|
cbIdx: number;
|
|
|
|
|
userData: number;
|
|
|
|
|
active: boolean;
|
|
|
|
|
period: bigint;
|
|
|
|
|
nextFire: bigint;
|
|
|
|
|
repeat: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SpiEntry {
|
|
|
|
|
device: SPIDevice;
|
|
|
|
|
cfg: SpiConfig;
|
|
|
|
|
onDoneCallback: (buffer: Uint8Array, count: number) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ChipInstanceOptions {
|
|
|
|
|
/** Compiled chip.wasm — either bytes, ArrayBuffer, or pre-compiled Module. */
|
|
|
|
|
wasm: Uint8Array | ArrayBuffer | WebAssembly.Module;
|
|
|
|
|
pinManager: PinManager;
|
|
|
|
|
i2cBus?: I2CBusManager | null;
|
|
|
|
|
spiBus?: SPIBus | null;
|
|
|
|
|
/** Logical chip pin name → real Arduino pin number (resolved from wires). */
|
|
|
|
|
wires?: Map<string, number>;
|
|
|
|
|
/** User-editable attributes — keyed by name. */
|
|
|
|
|
attrs?: Map<string, number>;
|
|
|
|
|
/** Returns simulation time in nanos (used by vx_sim_now_nanos). */
|
|
|
|
|
simNanos?: SimNanosFn;
|
|
|
|
|
/** Callback for chip log/printf output (defaults to console.log). */
|
|
|
|
|
log?: WriteStdoutFn;
|
|
|
|
|
/** Optional display dimensions from chip.json's `display` field. */
|
|
|
|
|
display?: { width: number; height: number } | null;
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
/** Optional external ROM bytes (vx_rom_size / vx_rom_read).
|
|
|
|
|
* Used by CPU-emulator chips that load their program from a project file
|
|
|
|
|
* instead of hard-coding it as a C byte array. */
|
|
|
|
|
romBytes?: Uint8Array | null;
|
2026-06-03 10:40:49 +07:00
|
|
|
/** Canvas component id of this chip. Used to key its SPICE pin sources so
|
|
|
|
|
* the analog engine drives the nets wired to the chip's output pins. */
|
|
|
|
|
componentId?: string;
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 10:40:49 +07:00
|
|
|
/** Logic-high voltage a chip output pin asserts on its SPICE net. */
|
|
|
|
|
const CHIP_OUTPUT_VCC = 5;
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
export class ChipInstance {
|
|
|
|
|
static MODE_OUTPUT_LOW = 16;
|
|
|
|
|
static MODE_OUTPUT_HIGH = 17;
|
|
|
|
|
|
|
|
|
|
private wasm: ChipInstanceOptions['wasm'];
|
|
|
|
|
private pinManager: PinManager;
|
|
|
|
|
private i2cBus: I2CBusManager | null;
|
|
|
|
|
private spiBus: SPIBus | null;
|
|
|
|
|
private wires: Map<string, number>;
|
|
|
|
|
private attrs: Map<string, number>;
|
|
|
|
|
private display: { width: number; height: number } | null;
|
2026-06-03 10:40:49 +07:00
|
|
|
private componentId: string;
|
2026-04-29 05:24:39 +07:00
|
|
|
|
|
|
|
|
memory: WebAssembly.Memory | null = null;
|
|
|
|
|
instance: WebAssembly.Instance | null = null;
|
|
|
|
|
exports: any = null;
|
|
|
|
|
disposed = false;
|
|
|
|
|
|
|
|
|
|
private pins: PinEntry[] = [];
|
|
|
|
|
private attrHandles: AttrEntry[] = [];
|
|
|
|
|
private _pinWatches = new Map<number, Set<() => void>>();
|
|
|
|
|
private timers: TimerEntry[] = [];
|
|
|
|
|
private uarts: UartConfig[] = [];
|
|
|
|
|
private _uartTxListener: ((byte: number) => void) | null = null;
|
|
|
|
|
private spiDevices: SpiEntry[] = [];
|
|
|
|
|
private _currentSpiBufPtr: number = 0;
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
private _romBytes: Uint8Array;
|
2026-04-29 05:24:39 +07:00
|
|
|
|
|
|
|
|
/** Framebuffer state — created on first vx_framebuffer_init call. */
|
|
|
|
|
private _framebuffer: { rgba: Uint8Array; width: number; height: number } | null = null;
|
|
|
|
|
private _onFramebufferUpdate: ((rgba: Uint8Array, w: number, h: number) => void) | null = null;
|
|
|
|
|
|
|
|
|
|
/** I2C device wrapper currently registered on the bus (for disposal). */
|
|
|
|
|
private _i2cDevice: { address: number } | null = null;
|
|
|
|
|
|
|
|
|
|
wasi: WasiShim;
|
|
|
|
|
private _velxioImports: Record<string, (...args: any[]) => any>;
|
|
|
|
|
|
|
|
|
|
static async create(opts: ChipInstanceOptions): Promise<ChipInstance> {
|
|
|
|
|
const inst = new ChipInstance(opts);
|
|
|
|
|
await inst._instantiate();
|
|
|
|
|
return inst;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(opts: ChipInstanceOptions) {
|
|
|
|
|
this.wasm = opts.wasm;
|
|
|
|
|
this.pinManager = opts.pinManager;
|
|
|
|
|
this.i2cBus = opts.i2cBus ?? null;
|
|
|
|
|
this.spiBus = opts.spiBus ?? null;
|
|
|
|
|
this.wires = opts.wires ?? new Map();
|
|
|
|
|
this.attrs = opts.attrs ?? new Map();
|
|
|
|
|
this.display = opts.display ?? null;
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
this._romBytes = opts.romBytes ?? new Uint8Array(0);
|
2026-06-03 10:40:49 +07:00
|
|
|
this.componentId = opts.componentId ?? '';
|
2026-04-29 05:24:39 +07:00
|
|
|
|
|
|
|
|
this.wasi = new WasiShim(
|
|
|
|
|
opts.simNanos ?? (() => 0n),
|
|
|
|
|
opts.log ?? ((s) => console.log(`[chip] ${s.replace(/\n$/, '')}`)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this._velxioImports = this._buildVelxioImports();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _instantiate(): Promise<void> {
|
2026-06-03 18:31:26 +07:00
|
|
|
// 4 pages (256 KB) initial: CPU-emulator chips like z80-cpu keep a 32 KB
|
|
|
|
|
// ROM + 32 KB RAM buffer as static data, which alone needs >2 pages once
|
|
|
|
|
// the WASM stack is added. Grows up to 16 pages on demand.
|
|
|
|
|
this.memory = new WebAssembly.Memory({ initial: 4, maximum: 16 });
|
2026-04-29 05:24:39 +07:00
|
|
|
this.wasi.setMemory(this.memory);
|
|
|
|
|
|
|
|
|
|
const importObject: WebAssembly.Imports = {
|
|
|
|
|
env: {
|
|
|
|
|
memory: this.memory,
|
|
|
|
|
...this._velxioImports,
|
|
|
|
|
},
|
|
|
|
|
...this.wasi.imports(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let module: WebAssembly.Module;
|
|
|
|
|
if (this.wasm instanceof WebAssembly.Module) {
|
|
|
|
|
module = this.wasm;
|
|
|
|
|
} else {
|
|
|
|
|
module = await WebAssembly.compile(this.wasm as BufferSource);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sanity-check imports so we surface a helpful error if something's missing.
|
|
|
|
|
const expected = WebAssembly.Module.imports(module);
|
|
|
|
|
const missing: string[] = [];
|
|
|
|
|
for (const imp of expected) {
|
|
|
|
|
const ns = (importObject as any)[imp.module];
|
|
|
|
|
if (!ns || ns[imp.name] === undefined) {
|
|
|
|
|
missing.push(`${imp.module}.${imp.name}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (missing.length) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Chip WASM imports missing in host:\n - ${missing.join('\n - ')}\n` +
|
|
|
|
|
`Extend WasiShim or ChipRuntime to provide them.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.instance = await WebAssembly.instantiate(module, importObject);
|
|
|
|
|
this.exports = this.instance.exports;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start(): void {
|
|
|
|
|
if (!this.exports?.chip_setup) {
|
|
|
|
|
throw new Error('Chip WASM does not export chip_setup');
|
|
|
|
|
}
|
|
|
|
|
this.exports.chip_setup();
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* Fire due timers up to sim-time `nowNanos`.
|
|
|
|
|
*
|
|
|
|
|
* `budgetMs` caps the wall-clock time spent in one call. A heavy multi-chip
|
|
|
|
|
* bus (e.g. a Z80 fetching from external ROM/RAM through the settle kernel)
|
|
|
|
|
* cannot run a real-time CPU clock in a single animation frame — without a
|
|
|
|
|
* cap the loop would fire tens of thousands of times and freeze the tab. With
|
|
|
|
|
* a budget the loop bails when exceeded, leaving each timer's nextFire where
|
|
|
|
|
* it is so the next call resumes from there: the simulation simply advances
|
|
|
|
|
* slower than real time (it boots over a few seconds) while the UI stays
|
|
|
|
|
* responsive. budgetMs = 0 (the default, used by headless tests) runs every
|
|
|
|
|
* due fire in one call.
|
|
|
|
|
*/
|
|
|
|
|
tickTimers(nowNanos: bigint | number, budgetMs = 0): void {
|
2026-04-29 05:24:39 +07:00
|
|
|
const now = BigInt(nowNanos);
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
if (!table) return;
|
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
|
|
|
const startWall = budgetMs > 0 ? performance.now() : 0;
|
2026-04-29 05:24:39 +07:00
|
|
|
for (const t of this.timers) {
|
|
|
|
|
if (!t.active) continue;
|
|
|
|
|
while (t.active && now >= t.nextFire) {
|
|
|
|
|
const fn = table.get(t.cbIdx) as ((ud: number) => void) | null;
|
|
|
|
|
if (fn) {
|
|
|
|
|
try { fn(t.userData); } catch { /* swallow chip errors */ }
|
|
|
|
|
}
|
|
|
|
|
if (t.repeat) {
|
|
|
|
|
t.nextFire += t.period;
|
|
|
|
|
} else {
|
|
|
|
|
t.active = 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
|
|
|
if (budgetMs > 0 && performance.now() - startWall > budgetMs) {
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
if (this.disposed) return;
|
|
|
|
|
for (const set of this._pinWatches.values()) {
|
|
|
|
|
for (const u of set) u();
|
|
|
|
|
}
|
|
|
|
|
this._pinWatches.clear();
|
|
|
|
|
this.timers = [];
|
|
|
|
|
if (this.i2cBus && this._i2cDevice) {
|
|
|
|
|
this.i2cBus.removeDevice(this._i2cDevice.address);
|
|
|
|
|
}
|
|
|
|
|
if (this.spiBus) {
|
|
|
|
|
for (const d of this.spiDevices) this.spiBus.removeDevice(d.device);
|
|
|
|
|
}
|
|
|
|
|
this.spiDevices = [];
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
// Stop driving any bus nets this chip contributed to, then re-resolve them
|
|
|
|
|
// so a removed chip releases the bus (its drivers no longer count).
|
|
|
|
|
if (this.componentId) clearBusDriversForChip(this.pinManager, this.componentId);
|
2026-04-29 05:24:39 +07:00
|
|
|
this.disposed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Build host imports table ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _buildVelxioImports(): Record<string, (...args: any[]) => any> {
|
|
|
|
|
return {
|
|
|
|
|
vx_pin_register: (namePtr: number, mode: number) => this._pin_register(namePtr, mode),
|
|
|
|
|
vx_pin_read: (handle: number) => this._pin_read(handle),
|
|
|
|
|
vx_pin_write: (handle: number, value: number) => this._pin_write(handle, value),
|
|
|
|
|
vx_pin_read_analog: (handle: number) => this._pin_read_analog(handle),
|
|
|
|
|
vx_pin_dac_write: (handle: number, voltage: number) => this._pin_dac_write(handle, voltage),
|
|
|
|
|
vx_pin_set_mode: (handle: number, mode: number) => this._pin_set_mode(handle, mode),
|
|
|
|
|
vx_pin_watch: (handle: number, edge: number, cbIdx: number, ud: number) =>
|
|
|
|
|
this._pin_watch(handle, edge, cbIdx, ud),
|
|
|
|
|
vx_pin_watch_stop: (handle: number) => this._pin_watch_stop(handle),
|
|
|
|
|
|
|
|
|
|
vx_attr_register: (namePtr: number, defaultVal: number) => this._attr_register(namePtr, defaultVal),
|
|
|
|
|
vx_attr_read: (handle: number) => this._attr_read(handle),
|
|
|
|
|
|
|
|
|
|
vx_i2c_attach: (cfgPtr: number) => this._i2c_attach(cfgPtr),
|
|
|
|
|
|
|
|
|
|
vx_uart_attach: (cfgPtr: number) => this._uart_attach(cfgPtr),
|
|
|
|
|
vx_uart_write: (handle: number, bufPtr: number, count: number) =>
|
|
|
|
|
this._uart_write(handle, bufPtr, count),
|
|
|
|
|
|
|
|
|
|
vx_spi_attach: (cfgPtr: number) => this._spi_attach(cfgPtr),
|
|
|
|
|
vx_spi_start: (handle: number, bufPtr: number, count: number) =>
|
|
|
|
|
this._spi_start(handle, bufPtr, count),
|
|
|
|
|
vx_spi_stop: (handle: number) => this._spi_stop(handle),
|
|
|
|
|
|
|
|
|
|
vx_sim_now_nanos: () => BigInt(this.wasi.simNanos() as number | bigint),
|
|
|
|
|
vx_timer_create: (cbIdx: number, ud: number) => this._timer_create(cbIdx, ud),
|
|
|
|
|
vx_timer_start: (handle: number, period: bigint, repeat: number) =>
|
|
|
|
|
this._timer_start(handle, period, repeat),
|
|
|
|
|
vx_timer_stop: (handle: number) => this._timer_stop(handle),
|
|
|
|
|
|
|
|
|
|
vx_framebuffer_init: (widthPtr: number, heightPtr: number) =>
|
|
|
|
|
this._framebuffer_init(widthPtr, heightPtr),
|
|
|
|
|
vx_buffer_write: (handle: number, offset: number, dataPtr: number, dataLen: number) =>
|
|
|
|
|
this._buffer_write(handle, offset, dataPtr, dataLen),
|
|
|
|
|
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
vx_rom_size: () => this._romBytes.length,
|
|
|
|
|
vx_rom_read: (offset: number, dstPtr: number, len: number) =>
|
|
|
|
|
this._rom_read(offset, dstPtr, len),
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
vx_log: (msgPtr: number) => {
|
|
|
|
|
const msg = readCString(this.memory!, msgPtr);
|
|
|
|
|
this.wasi.writeStdout(`[chip] ${msg}\n`);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
feat(chips): programmable retro CPU chips with external ROM
Adds a new way to use the retro CPU chips: write your program in a
project file (.s / .asm / .hex / .bin), click Compile, click Run, and
the same chip emulates whatever you wrote. Same chip + different ROMs =
mini PC, calculator, LED demo, Kill-the-Bit game, etc.
SDK:
- velxio-chip.h gets two new host imports:
uint32_t vx_rom_size(void);
void vx_rom_read(uint32_t off, uint8_t* dst, uint32_t len);
CPU-emulator chips call these in chip_setup to pull their program out
of the host's romBytes property.
Frontend runtime:
- ChipRuntime accepts opts.romBytes (Uint8Array) and exposes the new
imports, copying bytes into chip memory on vx_rom_read.
- CustomChipPart pulls component.properties.romBytes (base64) and passes
it through.
- Component registry declares three new custom-chip properties:
romBytes (base64), programFile (matching project filename), and
programTarget (cpu name).
New programmable bundled chip:
- frontend/src/components/customChips/examples/intel/i8080-cpu.{c,chip.json}
Same clean-room 8080 emulator as i8080-repl/i8080-counter, but ROM is
loaded externally via vx_rom_*. Has 8 LEDs, 8 buttons, UART, 16 KB RAM,
32 KB of external ROM.
Backend:
- New /api/compile-rom endpoint and rom_compile service that turns
chip-program source into ROM bytes. 8080 ASM is assembled by the
in-tree two-pass assembler (moved to backend/app/services/asm8080.py).
Intel HEX records are parsed; raw .bin is passed through. Future targets
(z80, 8086, 4004) are scaffolded but not wired yet.
EditorToolbar:
- Compile button detects when the active file is .s/.asm/.hex/.bin and
routes to compile-rom instead of arduino-cli. The compiled bytes are
injected into every custom-chip on the canvas whose programFile property
matches the active filename (or is empty).
Example:
- /examples/i8080-killbits loads Dean McDaniel's 1975 Kill-the-Bit on
the programmable i8080-cpu chip. killbits.s is shipped as a project
file alongside sketch.ino; the user clicks Compile then Run and the
LED walks across 8 outputs, buttons kill it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:38:18 +07:00
|
|
|
private _rom_read(offset: number, dstPtr: number, len: number): void {
|
|
|
|
|
if (!this.memory || this._romBytes.length === 0) return;
|
|
|
|
|
const max = this._romBytes.length;
|
|
|
|
|
if (offset >= max) return;
|
|
|
|
|
const end = Math.min(offset + len, max);
|
|
|
|
|
const dst = new Uint8Array(this.memory.buffer, dstPtr, end - offset);
|
|
|
|
|
dst.set(this._romBytes.subarray(offset, end));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
// ── Pin implementations ──────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-03 10:40:49 +07:00
|
|
|
/**
|
|
|
|
|
* Mirror an output pin's logic level into the SPICE chip-source registry and
|
|
|
|
|
* request a re-solve when it changes — so LEDs / analog parts wired to a chip
|
|
|
|
|
* output light up through ngspice, not just the digital PinManager path.
|
|
|
|
|
* Only synthetic chip pins (chip wired directly to components, no board GPIO
|
|
|
|
|
* on the net) are emitted as chip sources; a chip pin wired to a real board
|
|
|
|
|
* pin is already driven by that board's voltage source.
|
|
|
|
|
*/
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
/** True if this pin sits on a multi-chip BUS net (Phase 1): its key is a
|
|
|
|
|
* syntheticNetPin and the chipbus flag is on. Such pins resolve through the
|
|
|
|
|
* driver-strength registry (busNets) instead of last-writer-wins PinManager. */
|
|
|
|
|
private _isBusPin(p: PinEntry): boolean {
|
|
|
|
|
return p.arduinoPin != null && chipBusEnabled() && isSyntheticNetPin(p.arduinoPin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Register this pin's current (mode, value) as a bus driver and re-resolve. */
|
|
|
|
|
private _busDrive(p: PinEntry): void {
|
|
|
|
|
if (p.arduinoPin == null) return;
|
|
|
|
|
setBusDrive(
|
|
|
|
|
this.pinManager,
|
|
|
|
|
p.arduinoPin,
|
|
|
|
|
`${this.componentId}::${p.name}`,
|
|
|
|
|
modeToDrive(p.mode, p.value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 10:40:49 +07:00
|
|
|
private _syncSpiceDrive(p: PinEntry): void {
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
// A bus net is served by the digital driver-strength path; emitting a SPICE
|
|
|
|
|
// chip source per chip on the same net would create false analog contention.
|
|
|
|
|
if (this._isBusPin(p)) return;
|
2026-06-03 10:40:49 +07:00
|
|
|
if (!this.componentId || !p.name) return;
|
|
|
|
|
if (p.arduinoPin == null || !isSyntheticChipPin(p.arduinoPin)) return;
|
|
|
|
|
const isOutput =
|
|
|
|
|
p.mode === ChipInstance.MODE_OUTPUT_LOW || p.mode === ChipInstance.MODE_OUTPUT_HIGH;
|
|
|
|
|
const changed = isOutput
|
|
|
|
|
? setChipPinDrive(
|
|
|
|
|
this.componentId,
|
|
|
|
|
p.name,
|
|
|
|
|
this.pinManager.getPinState(p.arduinoPin) ? CHIP_OUTPUT_VCC : 0,
|
|
|
|
|
)
|
|
|
|
|
: setChipPinDrive(this.componentId, p.name, null);
|
|
|
|
|
if (changed) requestElectricalResolve();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
private _pin_register(namePtr: number, mode: number): number {
|
|
|
|
|
const name = readCString(this.memory!, namePtr);
|
|
|
|
|
const handle = this.pins.length;
|
|
|
|
|
const arduinoPin = this.wires.has(name) ? this.wires.get(name)! : null;
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
const value: 0 | 1 = mode === ChipInstance.MODE_OUTPUT_HIGH ? 1 : 0;
|
|
|
|
|
const p: PinEntry = { name, mode, arduinoPin, value };
|
2026-06-03 10:40:49 +07:00
|
|
|
this.pins.push(p);
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
if (this._isBusPin(p)) {
|
|
|
|
|
this._busDrive(p);
|
|
|
|
|
} else if (arduinoPin != null) {
|
2026-04-29 05:24:39 +07:00
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_LOW) this.pinManager.triggerPinChange(arduinoPin, false);
|
|
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_HIGH) this.pinManager.triggerPinChange(arduinoPin, true);
|
|
|
|
|
}
|
2026-06-03 10:40:49 +07:00
|
|
|
this._syncSpiceDrive(p);
|
2026-04-29 05:24:39 +07:00
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_read(handle: number): number {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p || p.arduinoPin == null) return 0;
|
|
|
|
|
return this.pinManager.getPinState(p.arduinoPin) ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_write(handle: number, value: number): void {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p || p.arduinoPin == null) return;
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
p.value = value !== 0 ? 1 : 0;
|
|
|
|
|
if (this._isBusPin(p)) {
|
|
|
|
|
this._busDrive(p);
|
|
|
|
|
} else {
|
|
|
|
|
this.pinManager.triggerPinChange(p.arduinoPin, value !== 0);
|
|
|
|
|
}
|
2026-06-03 10:40:49 +07:00
|
|
|
this._syncSpiceDrive(p);
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_read_analog(handle: number): number {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p || p.arduinoPin == null) return 0;
|
|
|
|
|
return this.pinManager.getPwmValue(p.arduinoPin) * 5.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_dac_write(handle: number, voltage: number): void {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p || p.arduinoPin == null) return;
|
|
|
|
|
this.pinManager.setAnalogVoltage(p.arduinoPin, voltage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_set_mode(handle: number, mode: number): void {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p) return;
|
|
|
|
|
p.mode = mode;
|
feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.
- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
driver wins; equal strength + opposite = X (contention); no driver = Z;
pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
the legacy path; dispose releases the chip's bus drivers. SPICE source
emission is skipped for bus pins (digital fast path beside SPICE).
Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:47 +07:00
|
|
|
// OUTPUT_LOW/HIGH carry an initial level; plain OUTPUT keeps the last value.
|
|
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_LOW) p.value = 0;
|
|
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_HIGH) p.value = 1;
|
|
|
|
|
if (this._isBusPin(p)) {
|
|
|
|
|
this._busDrive(p);
|
|
|
|
|
} else if (p.arduinoPin != null) {
|
2026-04-29 05:24:39 +07:00
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_LOW) this.pinManager.triggerPinChange(p.arduinoPin, false);
|
|
|
|
|
if (mode === ChipInstance.MODE_OUTPUT_HIGH) this.pinManager.triggerPinChange(p.arduinoPin, true);
|
|
|
|
|
}
|
2026-06-03 10:40:49 +07:00
|
|
|
this._syncSpiceDrive(p);
|
2026-04-29 05:24:39 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_watch(handle: number, edge: number, cbIdx: number, userData: number): void {
|
|
|
|
|
const p = this.pins[handle];
|
|
|
|
|
if (!p || p.arduinoPin == null) return;
|
|
|
|
|
let lastState = this.pinManager.getPinState(p.arduinoPin) ? 1 : 0;
|
|
|
|
|
const unsub = this.pinManager.onPinChange(p.arduinoPin, (_pin, state) => {
|
|
|
|
|
const newState = state ? 1 : 0;
|
|
|
|
|
const isRising = lastState === 0 && newState === 1;
|
|
|
|
|
const isFalling = lastState === 1 && newState === 0;
|
|
|
|
|
lastState = newState;
|
|
|
|
|
const wantRising = (edge & 1) !== 0;
|
|
|
|
|
const wantFalling = (edge & 2) !== 0;
|
|
|
|
|
if ((isRising && wantRising) || (isFalling && wantFalling)) {
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
if (!table) return;
|
|
|
|
|
const fn = table.get(cbIdx) as ((ud: number, pin: number, value: number) => void) | null;
|
|
|
|
|
if (fn) {
|
|
|
|
|
try { fn(userData, handle, newState); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!this._pinWatches.has(handle)) this._pinWatches.set(handle, new Set());
|
|
|
|
|
this._pinWatches.get(handle)!.add(unsub);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pin_watch_stop(handle: number): void {
|
|
|
|
|
const set = this._pinWatches.get(handle);
|
|
|
|
|
if (!set) return;
|
|
|
|
|
for (const u of set) u();
|
|
|
|
|
this._pinWatches.delete(handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Attributes ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _attr_register(namePtr: number, defaultVal: number): number {
|
|
|
|
|
const name = readCString(this.memory!, namePtr);
|
|
|
|
|
const handle = this.attrHandles.length;
|
|
|
|
|
this.attrHandles.push({ name, default: defaultVal });
|
|
|
|
|
if (!this.attrs.has(name)) this.attrs.set(name, defaultVal);
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _attr_read(handle: number): number {
|
|
|
|
|
const a = this.attrHandles[handle];
|
|
|
|
|
if (!a) return 0;
|
|
|
|
|
return this.attrs.get(a.name) ?? a.default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── I2C ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _i2c_attach(cfgPtr: number): number {
|
|
|
|
|
if (!this.i2cBus) {
|
|
|
|
|
throw new Error('Chip called vx_i2c_attach but no I2CBusManager is wired to the host');
|
|
|
|
|
}
|
|
|
|
|
const cfg = readI2CConfig(this.memory!, cfgPtr);
|
|
|
|
|
const callFn = (idx: number, ...args: any[]) => {
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
if (!table) return 0;
|
|
|
|
|
const fn = table.get(idx) as ((...a: any[]) => any) | null;
|
|
|
|
|
if (!fn) return 0;
|
|
|
|
|
try { return fn(...args); } catch { return 0; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let connectPending = true;
|
|
|
|
|
const device = {
|
|
|
|
|
address: cfg.address,
|
|
|
|
|
writeByte: (value: number): boolean => {
|
|
|
|
|
if (cfg.on_connect && connectPending) {
|
|
|
|
|
callFn(cfg.on_connect, cfg.user_data, cfg.address, 0);
|
|
|
|
|
connectPending = false;
|
|
|
|
|
}
|
|
|
|
|
const ack = !!callFn(cfg.on_write, cfg.user_data, value);
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
return ack;
|
|
|
|
|
},
|
|
|
|
|
readByte: (): number => {
|
|
|
|
|
if (cfg.on_connect && connectPending) {
|
|
|
|
|
callFn(cfg.on_connect, cfg.user_data, cfg.address, 1);
|
|
|
|
|
connectPending = false;
|
|
|
|
|
}
|
|
|
|
|
const b = callFn(cfg.on_read, cfg.user_data) & 0xff;
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
return b;
|
|
|
|
|
},
|
|
|
|
|
stop: (): void => {
|
|
|
|
|
if (cfg.on_stop) callFn(cfg.on_stop, cfg.user_data);
|
|
|
|
|
connectPending = true;
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.i2cBus.addDevice(device);
|
|
|
|
|
this._i2cDevice = device;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── UART ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _uart_attach(cfgPtr: number): number {
|
|
|
|
|
const cfg = readUartConfig(this.memory!, cfgPtr);
|
|
|
|
|
const handle = this.uarts.length;
|
|
|
|
|
this.uarts.push(cfg);
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _uart_write(handle: number, bufPtr: number, count: number): number {
|
|
|
|
|
const u = this.uarts[handle];
|
|
|
|
|
if (!u) return 0;
|
|
|
|
|
const u8 = new Uint8Array(this.memory!.buffer);
|
|
|
|
|
const bytes = u8.slice(bufPtr, bufPtr + count);
|
|
|
|
|
if (this._uartTxListener) {
|
|
|
|
|
for (const b of bytes) this._uartTxListener(b);
|
|
|
|
|
}
|
|
|
|
|
if (u.on_tx_done) {
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
const fn = table?.get(u.on_tx_done) as ((ud: number) => void) | null;
|
|
|
|
|
if (fn) {
|
|
|
|
|
try { fn(u.user_data); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
feedUart(byte: number, handle = 0): void {
|
|
|
|
|
const u = this.uarts[handle];
|
|
|
|
|
if (!u || !u.on_rx_byte) return;
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
const fn = table?.get(u.on_rx_byte) as ((ud: number, byte: number) => void) | null;
|
|
|
|
|
if (fn) {
|
|
|
|
|
try { fn(u.user_data, byte & 0xff); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUartTx(cb: (byte: number) => void): void {
|
|
|
|
|
this._uartTxListener = cb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** True if the chip declared at least one UART (post-chip_setup). */
|
|
|
|
|
get hasUart(): boolean {
|
|
|
|
|
return this.uarts.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── SPI ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _spi_attach(cfgPtr: number): number {
|
|
|
|
|
if (!this.spiBus) {
|
|
|
|
|
throw new Error('Chip called vx_spi_attach but no SPIBus is wired to the host');
|
|
|
|
|
}
|
|
|
|
|
const cfg = readSpiConfig(this.memory!, cfgPtr);
|
|
|
|
|
const handle = this.spiDevices.length;
|
|
|
|
|
const device = new SPIDevice();
|
|
|
|
|
|
|
|
|
|
const onDoneCallback = (_buffer: Uint8Array, count: number) => {
|
|
|
|
|
if (cfg.on_done) {
|
|
|
|
|
const table = this.exports?.__indirect_function_table as WebAssembly.Table | undefined;
|
|
|
|
|
const fn = table?.get(cfg.on_done) as ((ud: number, buf: number, c: number) => void) | null;
|
|
|
|
|
if (fn) {
|
|
|
|
|
try { fn(cfg.user_data, this._currentSpiBufPtr, count); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
this.wasi.flush();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.spiDevices.push({ device, cfg, onDoneCallback });
|
|
|
|
|
this.spiBus.addDevice(device);
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _spi_start(handle: number, bufPtr: number, count: number): void {
|
|
|
|
|
const entry = this.spiDevices[handle];
|
|
|
|
|
if (!entry) return;
|
|
|
|
|
const buf = new Uint8Array(this.memory!.buffer, bufPtr, count);
|
|
|
|
|
this._currentSpiBufPtr = bufPtr;
|
|
|
|
|
entry.device.startTransfer(buf, count, (b, c) => entry.onDoneCallback(b, c));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _spi_stop(handle: number): void {
|
|
|
|
|
const entry = this.spiDevices[handle];
|
|
|
|
|
if (!entry) return;
|
|
|
|
|
entry.device.stopTransfer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Framebuffer ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _framebuffer_init(widthPtr: number, heightPtr: number): number {
|
|
|
|
|
const w = this.display?.width ?? 128;
|
|
|
|
|
const h = this.display?.height ?? 64;
|
|
|
|
|
if (!this._framebuffer) {
|
|
|
|
|
this._framebuffer = { rgba: new Uint8Array(w * h * 4), width: w, height: h };
|
|
|
|
|
}
|
|
|
|
|
if (this.memory) {
|
|
|
|
|
const dv = new DataView(this.memory.buffer);
|
|
|
|
|
dv.setUint32(widthPtr, w, true);
|
|
|
|
|
dv.setUint32(heightPtr, h, true);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _buffer_write(_handle: number, offset: number, dataPtr: number, dataLen: number): void {
|
|
|
|
|
if (!this._framebuffer || !this.memory) return;
|
|
|
|
|
const src = new Uint8Array(this.memory.buffer, dataPtr, dataLen);
|
|
|
|
|
const dst = this._framebuffer.rgba;
|
|
|
|
|
const end = Math.min(offset + dataLen, dst.length);
|
|
|
|
|
const copyLen = Math.max(0, end - offset);
|
|
|
|
|
if (copyLen > 0) dst.set(src.subarray(0, copyLen), offset);
|
|
|
|
|
if (this._onFramebufferUpdate) {
|
|
|
|
|
try {
|
|
|
|
|
this._onFramebufferUpdate(this._framebuffer.rgba, this._framebuffer.width, this._framebuffer.height);
|
|
|
|
|
} catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Subscribe to framebuffer paint events. The callback fires after each
|
|
|
|
|
* vx_buffer_write, with the full RGBA buffer (consumer can blit it to a
|
|
|
|
|
* canvas). */
|
|
|
|
|
onFramebufferUpdate(cb: (rgba: Uint8Array, w: number, h: number) => void): void {
|
|
|
|
|
this._onFramebufferUpdate = cb;
|
|
|
|
|
// Fire once with the current state so the canvas reflects what's already there.
|
|
|
|
|
if (this._framebuffer) {
|
|
|
|
|
try { cb(this._framebuffer.rgba, this._framebuffer.width, this._framebuffer.height); } catch { /* swallow */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** True if the chip declared a framebuffer (post-chip_setup). */
|
|
|
|
|
get hasFramebuffer(): boolean {
|
|
|
|
|
return this._framebuffer !== null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 00:37:42 +07:00
|
|
|
// ── Keyboard (chips that export set_key, e.g. galaksija-keyboard) ─────────
|
|
|
|
|
|
|
|
|
|
/** True if the chip exposes a host-driven keyboard via an exported
|
|
|
|
|
* `set_key(offset, down)`. The host (CustomChipPart) bridges browser key
|
|
|
|
|
* events into it. */
|
|
|
|
|
get hasKeyboard(): boolean {
|
|
|
|
|
return typeof this.exports?.set_key === 'function';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Push a key state into the chip's key table. `offset` is the chip-specific
|
|
|
|
|
* matrix offset; `down` is press/release. No-op if the chip has no keyboard. */
|
|
|
|
|
setKey(offset: number, down: boolean): void {
|
|
|
|
|
try {
|
|
|
|
|
this.exports?.set_key?.(offset, down ? 1 : 0);
|
|
|
|
|
} catch {
|
|
|
|
|
/* swallow chip errors */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
// ── Timers ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private _timer_create(cbIdx: number, userData: number): number {
|
|
|
|
|
const handle = this.timers.length;
|
|
|
|
|
this.timers.push({ cbIdx, userData, active: false, period: 0n, nextFire: 0n, repeat: false });
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _timer_start(handle: number, periodNanos: bigint, repeat: number): void {
|
|
|
|
|
const t = this.timers[handle];
|
|
|
|
|
if (!t) return;
|
|
|
|
|
t.period = BigInt(periodNanos);
|
|
|
|
|
t.repeat = !!repeat;
|
|
|
|
|
t.nextFire = BigInt(this.wasi.simNanos() as number | bigint) + t.period;
|
|
|
|
|
t.active = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _timer_stop(handle: number): void {
|
|
|
|
|
const t = this.timers[handle];
|
|
|
|
|
if (t) t.active = false;
|
|
|
|
|
}
|
|
|
|
|
}
|