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):

  1. 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.
  2. Direct URL /visual/{ProjectId} — same project id used by the editor. Useful for share-links and Playwright tests. Renders the same VisualPanel with EmbeddedMode=false so 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.

  1. Drop a widget from the palette; it lands at a default position.
  2. Drag widgets to lay them out. Edit-mode only — drag is disabled during a run.
  3. 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).
  4. ▶ Run starts the coordinator. Press the Button widget (mousedown / touchstart → '1'; release → '0'). The LED widget reflects its bound port value each frame.
  5. Save writes a .kviz sidecar 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):

  • SimMode enum (Kmila.Shared/Services/SimulationParameters.cs) tags who owns the active run: SimMode.Signal for the singleton ProgramBuilder/DeltaCycleEngine path (background-persistent) vs SimMode.Visual for a visual-canvas run. SimulationParameters.RunningMode (set by BeginRun(projectId, mode)) lets the rest of the app distinguish the two without reaching into the canvas.
  • VisualRunRegistry (Services/VisualRun/VisualRunRegistry.cs) is an app-wide projectId → VisualRunCoordinator lookup. A visual run is owned as a private field on the component-scoped VisualPanel, so without this registry external surfaces (NavMenu / ProjectsView sidebar) could not reach it. VisualPanel registers on StartRun and unregisters on StopRun/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 wiringseg (7-bit) + sel (digits-bit); explicit wiringa..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 .kviz for 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 _switchStates dict; coordinator gets a userDrivenPorts set so it won't try to auto-clock switch-bound ports).
  • LedArrayWidget — configurable N segments, h/v orientation, MSB-on-left convention matching std_logic_vector(N-1 downto 0) string repr.
  • Auto-clock in VisualRunCoordinator: any IN port matching StimulusBuilder.IsClockPortName (clk, clock, aclk…) gets a square wave at CLOCK_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 modalbus[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-stop
  • visual-wire / visual-wiring-done
  • visual-add-led · visual-add-button · visual-add-switch · visual-add-led-array · visual-add-stepper · visual-add-seven-seg
  • visual-toggle-wires
  • visual-pause-continue
  • visual-bp-add
  • visual-delete / visual-save

Phase 2 acceptance walk: 5/5 pass at EN locale + spot-checked at ES.

7-segment display widget

Sixth widget; two operating modes set by the mode config key:

  • raw (default) — 8 individual pins (a b c d e f g dp). Student wires each segment to a port; behaves exactly like driving a 7447/4511 BCD-to-7-segment IC's segment outputs.
  • hex — 4-bit value pin, widget decodes 0..F internally to the canonical 7-seg pattern.
  • dec — same 4-bit value pin, 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.

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:

  1. Demo Board — every widget at once (the existing one-click sample is now the first entry in the catalog).
  2. Counter Clockcounter_clock entity, two decimal 7-segs counting 00 → 99 with an overflow LED on rollover. Demonstrates BCD-style state machines.
  3. Traffic Light FSMtraffic_light entity, three-state Moore machine (RED → GREEN → YELLOW), 3 colored LEDs + a 2-bit LED-array state-register readout.
  4. 8-bit Shift Register (added 2026-06-26 with slice UI) — shift_register entity, 8 individual LED widgets each wired to a single bit of an 8-bit reg_out port via reg_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

  • 1onrising_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 on VisualRunCoordinator.CustomTickDelayMs.
  • Pause / ContinuePause() / Resume() on the coordinator. The loop sleeps in 20 ms beats while IsPaused; 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 + fires OnBreakpointHit(tick). _lastBreakTick sentinel prevents re-fire on Resume.
  • Frame scrubbing — slider over [FrameRing.OldestTick, NewestTick]. Active only while Paused. Page calls FrameRing.Nearest(t) to look up the closest stored frame, then writes its PortValues into 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 at OnInitialized and takes Math.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.