Visual Runtime Mode
Complementary infinite-run simulation surface where VHDL design ports bind to a canvas of virtual hardware. Pedagogical goal: let students interact with their design — click a button, watch the LED — instead of reading waveforms after the fact.
The palette currently ships twelve widgets (2026-07-05): LED,
Button, Switch, Multi-Switch (2–8 lever DIP), LED array, LED matrix 3×3
(mono), RGB matrix 3×3, 7-segment, Buzzer / Piezo (Web Audio), Keypad
matrix, LCD HD44780 (2×16 / 4×20 · 4-bit / 8-bit / text modes), and
Stepper (4-phase). Registered in
Kmila.Shared/Services/VisualRun/KvizWidgetCatalog.cs (PinsOf) and
surfaced by Kmila.Shared/Components/VisualRun/WidgetPalette.razor.
Toolbar (2026-07-05): Back · ▶ Run / ⏹ Stop · Wire · Delete · Undo / Redo · Save · Clear changes · Show / hide wires · Zoom ± / Reset · Fit to view · Locator · Export SVG · Record video · ⋯ More · Fullscreen. Locator is a slide-out list of every placed widget with a per-row Focus button (snaps pan-zoom to that widget).
Canvas export — SVG · PNG · PDF, all shipped. SVG export is on the
toolbar; PNG and PDF live behind the ⋯ More menu
(OnMoreExportPng / OnMoreExportPdf in VisualPanel.razor). PNG shipped
in Session 22 and PDF shipped 2026-07-26 with jspdf 2.5.2 vendored
under wwwroot/lib/jspdf/. The old "deferred until html2canvas/jspdf are
vendored" note is obsolete: rather than html2canvas, the canvas is
rasterised by an in-house path (kmilaCanvasExportPng / kmilaCanvasExportPdf
in wwwroot/js/kmila-canvas-export.js, backed by kmilaSvgToPngBase64), and
the PDF path wraps that bitmap into a single-page document via jspdf's UMD
build (window.jspdf.jsPDF).
This document is the user / developer reference for the module. For
the design rationale and locked decisions see
HANDOFF_visual_runtime_2026-06-24.md. For the broader system pipeline
context see ARCHITECTURE.md § Visual Runtime.
1. Open the visual canvas
Two entry points, both pointing at the same underlying component
(Kmila.Shared/Components/VisualRun/VisualPanel.razor):
- Editor view-tab (shipped 2026-06-26) — open any project, the workbench tab strip exposes the canvas as the 4th view alongside Code · Blocks · Step replay · Visual. This is the only path that works in native MAUI (no address bar). The panel mounts lazily on the first tab visit, then survives subsequent flips so the coordinator + canvas state aren't lost when the user goes back to inspect code.
- Direct URL
/visual/{ProjectId}— same project id used by the editor. Useful for share-links and Playwright tests. Renders the sameVisualPanelwithEmbeddedMode=falseso the full header (back-button + project title) is shown.
The page parses the project's entry-point .vhd/.vhdl once on mount
and again on every Run click. If you edit VHDL in the Editor while
the visual canvas is open, click Run to pick the changes up — the canvas
does not hot-reload.
2. Building a canvas
The current toolbar is the one listed in the header above (Back · ▶ Run /
⏹ Stop · Wire · Delete · Undo / Redo · Save · Clear changes · Show / hide
wires · Zoom ± / Reset · Fit to view · Locator · Export SVG · Record video ·
⋯ More · Fullscreen). Widgets are not added from the toolbar anymore:
they come from the Widget palette
(Kmila.Shared/Components/VisualRun/WidgetPalette.razor), which exposes all
twelve widget types (see header). The old minimal "+ LED / + Button"
toolbar described in earlier revisions of this doc has been replaced.
- Drop a widget from the palette; it lands at a default position.
- Drag widgets to lay them out. Edit-mode only — drag is disabled during a run.
- Wire opens a modal listing every placed widget pin and the compatible design ports (direction-filtered: button outputs feed design inputs, LED inputs read design outputs / inout).
- ▶ Run starts the coordinator. Press the Button widget (mousedown / touchstart → '1'; release → '0'). The LED widget reflects its bound port value each frame.
- Save writes a
.kvizsidecar at the project root (one per project). The sidecar persists positions, configs, bindings.
3. .kviz schema (v1)
JSON, alongside the design's .vhd/.vhdl. The simulator never reads
this file; it's purely the visual canvas state.
{
"version": 1,
"designFile": "main.vhd",
"canvas": { "width": 1200, "height": 800, "background": "grid" },
"components": [
{
"id": "led-ab12cd",
"type": "led",
"position": { "x": 320, "y": 180 },
"config": { "color": "red" },
"bindings": { "anode": "led_out" }
},
{
"id": "btn-34ef56",
"type": "button",
"position": { "x": 120, "y": 180 },
"config": { "label": "GO" },
"bindings": { "out": "btn_in" }
}
],
"settings": { "ringBufferTicks": 2000, "defaultSpeed": "Normal" }
}
| Field | Phase 0 valid values |
|---|---|
components[].type |
led, button |
components[].bindings keys |
LED: anode. Button: out. |
settings.defaultSpeed |
Half / Normal / Fast / Free |
Phase 1 extends type to switch, led-array, seven-seg; adds slice
syntax to binding values (bus[3:0], bus[7]).
4. The seam in one paragraph
Kmila.Shared/Services/VisualRun/VisualRunCoordinator.cs drives an
existing Interpreter.Services.DeltaCycleEngine one tick at a time.
Between ticks it drains its InputStateMap into the engine's input
signals by setting Variable.Value directly — this triggers
SignalAttributeHandler._activeFlags[name] = true, which the engine
checks at the top of every RunTimeStepAsync and propagates as a
normal external-stimulus event. No new engine hook is needed —
the existing event/scheduler path carries live writes from a paused-
between-ticks coordinator just as well as from the pre-built stimulus
list StimulusBuilder produces for bounded runs. Bounded
HeadlessSimulator / SimulationRunner / SimulatorBench are not
touched by anything in this module.
4.bis. Background-run prevention + external control (2026-08-11)
Shipped in the CHANGELOG entry "Visual-sim background prevention + toolbar polish — 2026-08-11" (device-verified on Xiaomi 13T Pro):
SimModeenum (Kmila.Shared/Services/SimulationParameters.cs) tags who owns the active run:SimMode.Signalfor the singletonProgramBuilder/DeltaCycleEnginepath (background-persistent) vsSimMode.Visualfor a visual-canvas run.SimulationParameters.RunningMode(set byBeginRun(projectId, mode)) lets the rest of the app distinguish the two without reaching into the canvas.VisualRunRegistry(Services/VisualRun/VisualRunRegistry.cs) is an app-wideprojectId → VisualRunCoordinatorlookup. A visual run is owned as a private field on the component-scopedVisualPanel, so without this registry external surfaces (NavMenu / ProjectsView sidebar) could not reach it.VisualPanelregisters onStartRunand unregisters onStopRun/Dispose; at most one coordinator per project is tracked (a new registration replaces the prior one).- Effect: a visual run no longer keeps ticking invisibly after the user navigates away — the run can be paused/stopped from outside the canvas (e.g. a sidebar Stop), and tab/visibility changes are handled so the canvas does not run unattended in the background.
5. Component contract
Each widget is a Razor component under
Kmila.Shared/Components/VisualRun/ with a typed pin list and a
direction:
| Component | Pin | Direction | Config |
|---|---|---|---|
LedWidget (Phase 0) |
anode |
in (reads design output) | color: red \| green \| blue \| yellow \| white |
ButtonWidget (Phase 0) |
out |
out (drives design input) | label: string |
SwitchWidget (Phase 1) |
out |
out (drives design input) | label: string, defaultState: 0 \| 1 |
LedArrayWidget (Phase 1) |
anodes |
in (reads design output, multi-bit) | length: 1..64, color, orientation: h \| v |
StepperWidget (Phase 2) |
phaseA, phaseB, phaseAn, phaseBn |
in (4 single-bit inputs) | stepsPerRev: 4..4096 (default 200), direction: auto \| cw \| ccw |
SevenSegmentWidget (Phase 3) |
raw mode: a, b, c, d, e, f, g, dp (8 single-bit in) · decoded mode: value (4-bit std_logic_vector in) + optional dp |
in | mode: raw \| hex \| dec (default raw), polarity: cathode \| anode (default cathode, raw mode only — anode = active-low), color (same enum as LED) |
SevenSegmentBankWidget (Ring 5) |
raw/hex/dec: value (single packed bus in) — raw width digits×7 (each 7-bit slice abcdefg), decoded width digits×4 · mux (multiplexed, shared segments + per-digit select): packed wiring → seg (7-bit) + sel (digits-bit); explicit wiring → a..g + an0..an{digits-1} (all single-bit) |
in | digits: 2..8 (default 4), mode: raw \| hex \| dec \| mux (default raw), polarity: cathode \| anode (default cathode; raw + mux segment lines), color; mux only: wiring: packed \| explicit (default packed), selectActiveLow (bool, default true — digit enabled when its select reads 0), persistence (bool, default true — latch each digit across recent frames vs show the instantaneous strobe) |
Width-mismatch is natural truncation by design (handoff §3 decision 4). A 1-LED bound to an 8-bit bus reads bit 0 only; a 1-bit signal bound to a (future) LED array drives only LED 0.
6. Performance budget
Default speed (Normal) targets ≈60 Hz: one tick every 16 ms with a
30 Hz UI repaint throttle on the page side (33 ms timer in
Visual.razor). At Free speed the coordinator yields every 256 ticks.
The ring buffer (default 2000 entries) is per-coordinator and caps live memory; older frames evict. With ~10 ports per frame and ~64 bytes per value, that's roughly 1.2 MB at full capacity — well inside the 3 GB emulator ceiling.
7. Phase 0 acceptance walk — CLOSED 2026-06-24
All 12 handoff §9 items pass on both targets (Web fully, MAUI structurally).
SimulatorBench unchanged at 119/119 PASS. Engine and bounded-path code
untouched by construction. Full progress log including surprises in
HANDOFF_visual_runtime_2026-06-24.md § 9.
Test coverage that ships with Phase 0:
Tests/Playwright/tests/11-visual/seed.spec.ts— creates 4 fixture projects (passthrough / inverter / and_gate / sr_latch) via the UI. Idempotent.Tests/Playwright/tests/11-visual/wire_and_save.spec.ts— pins the passthrough fixture's bindings into.kvizfor any downstream test that needs a pre-wired canvas.Tests/Playwright/tests/11-visual/acceptance.spec.ts— six tests covering §9.1, §9.2, §9.4 (mouse + touch), §9.5, §9.6/§9.7/§9.8 combined, §9.9, §9.10.
Latency observation (handoff §10): Web target latency ≈ 100-150 ms (SignalR + 30 Hz repaint throttle + Playwright poll cadence). Tablet real-touch press → LED-on is visibly the next frame. The 50 ms target in §9.7 stands as the engine-side budget; the Web roundtrip is the tax.
8. Phase 1A/B shipped 2026-06-25 — Switch + LED array
Two new components, plus coordinator auto-clock so clocked designs run without an external clock source:
SwitchWidget— toggle on tap, state persists across Run/Stop cycles (page-level_switchStatesdict; coordinator gets auserDrivenPortsset so it won't try to auto-clock switch-bound ports).LedArrayWidget— configurable N segments, h/v orientation, MSB-on-left convention matchingstd_logic_vector(N-1 downto 0)string repr.- Auto-clock in
VisualRunCoordinator: any IN port matchingStimulusBuilder.IsClockPortName(clk,clock,aclk…) gets a square wave atCLOCK_HALF_PERIOD_TICKS = 8(≈256 ms cycle at Normal speed). Switch/Button-bound ports excluded — user drives them.
Phase 1 remaining (deferred):
- 7-seg single + N-digit (raw + assisted decode bin/hex/BCD).
- Slice UI in wiring modal —
bus[3:0],bus[7]dropdown options. - Button bounce toggle + chatter generator.
10. Phase 3 partial — i18n + wire visualization + demo board (2026-06-26)
Three deliverables ship together in this slice — closes 2 of 4 Phase 3 roadmap items + the i18n audit finding + a new "showcase everything" default sample project.
Demo Board sample (Try a sample flow)
ProjectsView.razor rewrites SAMPLE_MAIN_VHD from the 7-stage ripple
counter to a demo_board entity that exercises every Visual widget at
once: Buttons (reset, nudge), Switches (run, dir), LEDs
(heartbeat, btn_echo), LED array (count), Stepper
(phase_a/b/an/bn). A new SAMPLE_MAIN_KVIZ constant ships the
companion pre-wired .kviz so opening the Visual tab is immediately
playable — no Wiring modal traversal needed.
The bounded-sim port moments in SaveSampleConfigurationAsync also
updated to drive the new 5-input shape (clk, reset, run, dir,
nudge). 30 µs total run shows the counter rolling, the heartbeat
toggling, the stepper phasing, and a single late nudge pulse echoing
on btn_echo.
Dictionary updates: PROJECTS_SAMPLE_NAME → "Sample · Demo board" (EN/ES
native, FR/DE/JP/CH/AR ⚠-prefixed). PROJECTS_SAMPLE_HINT and
PROJECTS_SAMPLE_DESCRIPTION rewritten to advertise the visual surface.
Visual page i18n
~50 string keys added to all 7 dictionaries under the VISUAL_*
namespace. EN+ES translated natively; FR/DE/JP/CH/AR carry the ⚠ prefix
per the translation-pending convention. VisualPanel.razor and
VisualWiringModal.razor no longer contain hardcoded English — all
labels, hints, error strings, properties pane options route through
Traductor.T().
Wire visualization overlay
New Show wires / Hide wires toggle on the toolbar. When on, an SVG
overlay (positioned with pointer-events: none so widget interaction
stays untouched) draws dashed accent-colored lines from each placed
widget's anchor to a port-name chip pinned near the right canvas edge.
Chips stack vertically so multiple wires near the same Y don't overlap.
Per-session state (_showWires field on VisualPanel) — opt-in each
time the user opens the canvas; not persisted to .kviz so the saved
file stays focused on layout + bindings only.
The overlay SVG inner content is built via MarkupString because
Razor reserves the lowercase <text> tag for its own escape-from-C#
construct and refuses to emit an SVG <text> element directly.
Test infrastructure
The i18n pass meant every :text-is(...) and :has-text(...) toolbar
selector in phase2.spec.ts became locale-dependent (the server's
default-language setting determines what text renders). Migrated all
toolbar + runbar selectors to data-testid="visual-...", future-proof
across all 7 locales:
visual-run/visual-stopvisual-wire/visual-wiring-donevisual-add-led·visual-add-button·visual-add-switch·visual-add-led-array·visual-add-stepper·visual-add-seven-segvisual-toggle-wiresvisual-pause-continuevisual-bp-addvisual-delete/visual-save
Phase 2 acceptance walk: 5/5 pass at EN locale + spot-checked at ES.
11. Phase 3+ — 7-segment widget + example gallery (2026-06-26)
7-segment display widget
Sixth widget; two operating modes set by the mode config key:
raw(default) — 8 individual pins (abcdefgdp). Student wires each segment to a port; behaves exactly like driving a 7447/4511 BCD-to-7-segment IC's segment outputs.hex— 4-bitvaluepin, widget decodes 0..F internally to the canonical 7-seg pattern.dec— same 4-bitvaluepin, displays 0..9 and blanks (no segments lit) on any value > 9. Matches what a real 4511 BCD decoder does on out-of-range inputs.
Both decoded modes still expose a dp pin so the user can drive the
decimal point separately.
Mode changes in the properties pane drop bindings that don't apply to
the new pin set so the user isn't left with orphaned wires (raw keeps
a..g + dp; decoded modes keep value + dp).
Demo Board sample now includes a hex-decoded 7-segment bound to the 4-bit counter output — first thing the student sees after picking the sample is a digit display ticking through 0–F next to the LED array.
Example gallery
New "Examples" button next to "Try a sample" opens a modal listing the
pre-wired catalog. One click creates the project (with its .kviz
sidecar) and jumps into the editor. Catalog ships:
- Demo Board — every widget at once (the existing one-click sample is now the first entry in the catalog).
- Counter Clock —
counter_clockentity, two decimal 7-segs counting 00 → 99 with an overflow LED on rollover. Demonstrates BCD-style state machines. - Traffic Light FSM —
traffic_lightentity, three-state Moore machine (RED → GREEN → YELLOW), 3 colored LEDs + a 2-bit LED-array state-register readout. - 8-bit Shift Register (added 2026-06-26 with slice UI) —
shift_registerentity, 8 individual LED widgets each wired to a single bit of an 8-bitreg_outport viareg_out[N]slice bindings. Showcases the bit-slice wiring UI.
Each example has stable id (demo / counter / traffic / shift)
for future share-links + analytics. Adding a new example = add an
ExampleDef entry to the _examples list in ProjectsView.razor.
12. Phase 3++ — bit-slice UI (2026-06-26)
Closes one more Phase 1C deferred item from HANDOFF §8.
Binding-value syntax
Binding values in .kviz now accept an optional slice suffix:
| Binding | Means |
|---|---|
count |
whole port (existing behavior) |
count[3] |
single bit (LSB-numbered, std_logic_vector convention) |
count[7:0] |
bit range, MSB-first to match VHDL downto syntax |
The slice operates on the port's string representation — engine emits
MSB-on-left for std_logic_vector(N-1 downto 0), so a 4-bit 0xA
renders as "1010" (bit3 bit2 bit1 bit0) and [2] picks position 2
from the right = '0', [3:1] picks "101".
Out-of-range slices return empty (widget treats as unwired / blank).
Coordinator API
coordinator.LatestSlicedValue("count[3]") // → "0" / "1"
coordinator.LatestSlicedValue("bus[7:4]") // → 4-char MSB-on-left
coordinator.LatestSlicedValue("count") // → whole port (passthrough)
SliceHelper.Parse(binding) parses the suffix; SliceHelper.Extract
returns the substring. All output widgets (LED, LED-array, 7-segment,
Stepper) call LatestSlicedValue instead of LatestPortValue so any
of them transparently accept sliced bindings.
Wiring modal — per-bit dropdown options
For multi-bit OUT ports binding into a SINGLE-BIT widget pin (anode
on LED, segments on raw-mode 7-seg, phase pins on Stepper), the
dropdown now expands to:
port(whole, MSB used)port[N](one entry per bit, descending)
For multi-bit widget pins (anodes on LED-array, value on
decoded-mode 7-seg), the dropdown offers natural ranges (port[3:0] /
port[7:4]).
Write-side slice — deferred
Input widgets (Switch, Button) still drive whole ports — write-side multi-input merging (two switches each claiming a different bit of the same port) needs coordinator-level overlay logic that's a bigger lift. Designs needing fine-grained input control still declare individual single-bit IN ports for now.
13. Button bounce (2026-06-26)
Closes the last Phase 1C-deferred item from HANDOFF §8.
ButtonWidget gains a config.bounce: bool (default false) — when
enabled, every press + release generates a 6-tick alternating chatter
pattern before the port settles on the steady intent. Real mechanical
buttons bounce 5–20 ms; this exaggeration (≈ 96 ms at Normal speed)
gives debouncer practice exercises something visible to debounce.
How it works
VisualRunCoordinator.EnqueueBounce(portName, chatter) enqueues a
FIFO of per-tick values. DrainInputsToPorts overlays the queue on
top of InputStateMap — for one tick at a time, the bound port reads
the queued value instead of the steady-state intent. Once the queue
drains, the steady value reasserts.
Re-pressing during chatter cleanly truncates and re-arms — same as a real button when you re-press it before the release wobble settles.
Properties pane
Buttons get a "Simulate contact bounce" checkbox under the label
input. Toggling on writes "bounce": true into the widget's .kviz
config; the canvas saves dirty.
Suggested practice
Wire a bouncy button to a port that drives a counter `count <= count
- 1
onrising_edge(btn)`. Without bouncing, one press increments once. With bouncing, one press increments 3 times. Then write a simple FF-based debouncer (sample at ~10 ms intervals, count consecutive stable readings) and watch it filter the chatter back to a single edge.
9. Phase 2 shipped 2026-06-25 — controls + stepper
Five locked items from HANDOFF §8 all in the same session:
- Speed slider — native
<input type="range">over 0..3 mapping Half / Normal / Fast / Free. Replaces the four buttons. - Custom step rate —
<input type="number">ms/tick field that overrides the slider preset. Empty input → slider preset active. Set onVisualRunCoordinator.CustomTickDelayMs. - Pause / Continue —
Pause()/Resume()on the coordinator. The loop sleeps in 20 ms beats whileIsPaused; widgets stop repainting because no new frames push. - Tick breakpoints — numeric breakpoint inputs collected into
VisualRunCoordinator.TickBreakpoints(HashSet). The loop checks membership at the top of each tick; on hit it sets IsPaused = true+ firesOnBreakpointHit(tick)._lastBreakTicksentinel prevents re-fire on Resume. - Frame scrubbing — slider over
[FrameRing.OldestTick, NewestTick]. Active only while Paused. Page callsFrameRing.Nearest(t)to look up the closest stored frame, then writes itsPortValuesinto the engine ports → widgets re-read on the next repaint. Resume → next live tick overwrites scrubbed values. StepperWidget— 4 single-bit phase inputs decoded against a Gray-code table (8 patterns). CSS-rotated rotor + 4 coil pads (N/E/S/W). Auto direction inference from wrapped step delta; cw/ccw force the sign.- Settings → Apariencia → Simulación → ring buffer (ticks) — new
SQLite key
KMILA_VISUAL_RING_BUFFER(256..16384, default 2000). Visual page reads atOnInitializedand takesMath.Max(perDoc, settings)for the coordinator's ring capacity.
Phase 2 deferred (none — all 5 items shipped).
Phase 3+ roadmap (unchanged):
- Phase 3 — property inspector, wire visualization, example canvases, full i18n.