This content isn't available in your language yet; showing English.

Kmila-9s Architecture

Cross-Platform Application for Learning and Debugging VHDL Code without Physical Hardware Requirements


flowchart TB
    Usuario([Usuario])

    subgraph App["Aplicación Multiplataforma (MAUI / Blazor)"]
        UI[Interfaz de Usuario<br/>Editor Monaco, Visualizador, i18n]
        ProgramBuilder[ProgramBuilder<br/>Orquestador de Construcción]
    end

    subgraph Core["Motor de Simulación"]
        Debugger[Debugger<br/>Orquestador de Simulación]
        Parser[Parser<br/>Análisis Léxico y Sintáctico]
        Interpreter[Intérprete<br/>Ejecución y Ciclos Delta]
        TimeMachine[TimeMachine<br/>Control de Tiempo Discreto]

        subgraph IServ["Servicios del Intérprete (singletons por proceso)"]
            EntityReg[(EntityRegistry)]
            PackageReg[(PackageRegistry)]
            FunctionReg[(FunctionRegistry<br/>v1.15)]
            SkipLog[/SkipDiagnosticLog<br/>v1.15/]
            IEEELoader[/"IEEELibraryLoader<br/>v1.15 #20"/]
        end
    end

    subgraph Lib["LibraryCompiler (módulo hermano, v1.16 #22-#26)"]
        LCFacade[LibraryCompiler<br/>Facade + ProjectContext]
        Resolver[LibraryResolver<br/>topo-sort de dependencias]
        Builder[LibraryBuilder<br/>una librería → CompiledLibrary]
        Orch[BuildOrchestrator<br/>Linear / Parallel +<br/>OnDemand/Cached/Persistent]
        IBlobStore[/IBlobStore<br/>contrato; concretos en host/]
        IEEEStubs[(Builtins/IEEE/<br/>std_logic_1164.vhd<br/>numeric_std.vhd<br/>embedded resources)]
    end

    LCFacade --> Resolver
    LCFacade --> Orch
    Orch --> Builder
    Builder -.-> IEEEStubs
    Orch <-.-> IBlobStore
    IEEELoader -->|delega v1.16| LCFacade
    LCFacade -->|seed CompiledLibrary| FunctionReg

    subgraph Data["Capa de Datos"]
        SQLite[(SQLite<br/>Proyectos y Configuración)]
        FPGA_JSON[(JSON<br/>Especificaciones FPGA)]
    end

    subgraph Output["Resultados"]
        Signals[Estados de Señales<br/>y Formas de Onda]
        Resources[Estimación de<br/>Recursos FPGA]
    end

    Usuario --> UI
    UI --> ProgramBuilder
    ProgramBuilder --> Debugger
    Debugger --> Parser
    Parser -->|AST| Interpreter
    Debugger --> Interpreter
    Interpreter <--> TimeMachine
    Interpreter --> Signals
    Interpreter --> Resources
    Signals --> UI
    Resources --> UI
    UI --> SQLite
    UI --> FPGA_JSON

    style Usuario fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style UI fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style ProgramBuilder fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style Debugger fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Parser fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Interpreter fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style TimeMachine fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style SQLite fill:#fce4ec,stroke:#c62828,stroke-width:2px
    style FPGA_JSON fill:#fce4ec,stroke:#c62828,stroke-width:2px
    style Signals fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style Resources fill:#fff3e0,stroke:#e65100,stroke-width:2px

Module Description
App Cross-platform interface (MAUI/Blazor) with Monaco editor, project management, i18n (7 languages), and FPGA resource validation
Debugger Pre-processing layer (not an orchestrator). Reads a .vhdl, runs a lint (RunDiagnosticsVHD-MISSING-SEMICOLON, VHD-BLOCK-BALANCE) and a structural pass that hydrates Interpreter types (Entity/Architecture). It does not run the simulation nor call TimeMachine; the live simulation flow is handled by Kmila.Shared.Services against the Interpreter. It propagates line indices to the Tokenizer (TokenizeLineAt) and publishes Diagnostics even when the structural parse throws an exception. (Correct as of v1.14.1: earlier revisions described a "central orchestrator / State Manager / Time Travel" role that never existed — see ../Debugger/README.md.)
Parser Analyzes the VHDL code: lexical analysis (Tokenizer), syntactic analysis, and generation of the Abstract Syntax Tree (AST). In v1.15 it accepts procedure declarations in the architecture header, direct entity instantiation (label : entity work.X(arch)), and arithmetic expressions as generic default values.
Interpreter Executes the VHDL logic with a Shunting-yard algorithm, delta cycles, and management of signal states. v1.15 introduces FunctionRegistry (signatures of functions and procedures as black boxes), SkipDiagnosticLog (VHD-* codes for every construct silently omitted), and IEEELibraryLoader, which seeds the names of IEEE.std_logic_1164 and IEEE.numeric_std (delegated to the LibraryCompiler module as of v1.16 #22-#26). It supports case <slice>, <sig>'range in for-loops, and aliases with effective size. Tier 4.1–4.3 engine (closed 2026-07-28): signal attributes 'event/'last_value/'stable[(T)]/'active (SignalAttributeHandler + DeferredAttribute); case?/is? (VHDL-2008 matching-case); body-synthesis MVP of functions/procedures (single-return inlining + if/elsif/else cascade → MUX, with ProcedureFrame/LoopFrame/BranchFrame/IProcessFrame frames and UserFunctionBlackBox fallback). Two interchangeable runners via ISimulationRunner: SimulationRunner (Visual/Slow) and FastSimulationRunner (Fast, skips idle ticks, byte-identical).
TimeMachine Controls discrete simulation time (10 ns/tick), clock cycles, and provides execution control (start/pause/stop). v1.15 replaces the interactive REPL of Program.cs with an automated test-runner of 23 cases (Clock, TimeClock, Timer + 3 stress).

Platforms: Windows, macOS, Linux, Android, iOS | Stack: .NET 9, MAUI, Blazor | Database: SQLite

Memory profile (v1.15 baseline, median peak RSS — not re-measured since): Parser 94 MB · Interpreter 101 MB · Interpreter --test-delta 56 MB · Debugger 98 MB · TimeMachine 37 MB. All executable .csproj files declare ServerGarbageCollection=false to favor Workstation GC on low-RAM devices. (v1.15 reference figures; the engine has grown with Tier 4.1–4.3, so treat them as order-of-magnitude, not as current values.)

Release notes: CHANGELOG_v1.15.mdCHANGELOG_v1.19.md and the per-topic CHANGELOG_*_2026-*.md now live in _archive/changelogs/ and are merged into the consolidated CHANGELOG.md (freshest source). Work after July is summarized in STATUS.md §0.

Button system — slot grammar (2026-06-28). Three button families (.kmila-btn Family 11 · .kmila-btn-icon Family 14 · .kmila-btn-tab Family 15) live in Kmila.Shared/wwwroot/css/buttons.css. On top of them rests a slot grammar that dictates where each button goes, what size it takes for its role, and how it groups with its neighbors:

  • .kmila-slot-input-trailing — the input absorbs the width, the button shares the row at align-items: stretch; it never drops below.
  • .kmila-slot-banner — text + action at the end of the banner; the text wraps, the button never wraps.
  • .kmila-slot-modal-footer — action row aligned to the right at the bottom of the modal; ≤520 px stacks inverted for thumb reach.
  • .kmila-slot-header-tools (+ __filter, __filter-divider, __actions) — filter shelf (search ± select integrated into a 44 px container) + separate actions cluster. Reflows to two rows at ≤768 px.
  • .kmila-slot-segmented — a pill background unifies a row of .kmila-btn-tab so it reads as a single segmented control.
  • .kmila-slot-toolbar-divider / .waveform-viewer__divider — thin vertical separator between clusters within the same toolbar.

Rules not codified as classes (variant rationalization, swap Family 11 → 14 in list rows, retire the orphan .btn-lg, single-family-per-toolbar) are documented in CHANGELOG_button_placement_2026-06-28.md and in the frozen proposal at Documentacion/UI_AUDIT_2026-06-28-button-placement/SUMMARY.md.


Mini-curriculum of Concepts (/concepts, 2026-05-17)

A track parallel to /learn and /practice, dedicated to the execution model behind VHDL: sequential within a process, concurrent in the body of the architecture, and why real parallelism only exists in hardware. Canonical documentation in ROADMAP_concurrent_execution_module.md; implementation details in CHANGELOG_concepts_module_2026-05-17.md.

flowchart LR
    Catalog["Pages/Concepts.razor<br/>/concepts"] --> Lesson["Pages/ConceptView.razor<br/>/concepts/{module}/{lesson}"]
    Catalog --> Exercise["Pages/ConceptExerciseView.razor<br/>/concepts/exercise/{id}"]

    Lesson --> Player["Components/ExecutionPlayer.razor<br/>Monaco read-only + tape transport"]
    Lesson --> Quiz["Components/QuizletPanel.razor<br/>radio + check + explicación"]

    Exercise --> Editor["Components/BlockEditor.razor<br/>kmila-blocks workspace"]
    Exercise -->|fingerprint match| Player

    Player --> Runner["Services/TimelineRunner<br/>state machine, no DI"]
    Editor --> BP["Services/BlockProgram<br/>fingerprint(workspaceJson)"]

    Catalog -.-> Cat["Services/ConceptCatalog<br/>manifest + bodies + timelines + payloads"]
    Lesson -.-> Cat
    Exercise -.-> Cat
    Lesson -.-> Prog["Services/ConceptProgressStore<br/>concept.done.* en AppSettings"]
    Exercise -.-> Prog

    style Catalog fill:#e3f2fd,stroke:#1565c0
    style Lesson fill:#e3f2fd,stroke:#1565c0
    style Exercise fill:#e3f2fd,stroke:#1565c0
    style Player fill:#fff3e0,stroke:#e65100
    style Editor fill:#fff3e0,stroke:#e65100
    style Quiz fill:#fff3e0,stroke:#e65100
    style Runner fill:#e8f5e9,stroke:#2e7d32
    style BP fill:#e8f5e9,stroke:#2e7d32
    style Cat fill:#f3e5f5,stroke:#7b1fa2
    style Prog fill:#f3e5f5,stroke:#7b1fa2

Key design decisions:

Decision Rationale
Catalog and store cloned from LessonCatalog/LessonProgressStore instead of parameterized Isolation: the Concepts track can evolve its schema without risk of regression in /learn. Same code cost, much smaller blast radius.
Timelines pre-recorded in JSON (Phase 1) instead of instrumenting the simulator Lets each step carry a didactic note and lets the narrative pace be controlled; the simulator needs no changes. Phase 2 of the roadmap can substitute live emission without touching the ExecutionPlayer.
kmila-blocks (bespoke JS engine, wwwroot/js/kmila-blocks*.js) replaces Blockly as of v1.20 A single kmila-blocks.js (~36 KB) replaced the Blockly bundle (~1,043 KB). The interop names kmilaBlockly* are preserved for historical compatibility; the engine is kmila-blocks, not Blockly. See [[project_blockly_replacement_plan]].
Canonical fingerprint (workspace JSON → ordered string) instead of structural comparison Allows small pre-assembled catalogs and a friendly "this combination is not ready yet" message when the student builds something outside the set. It is order-sensitive — commutative variants require additional entries in the catalog.
Quizlets as an optional [Parameter] in Concept Three lessons (1, 3, 5) carry a validation gate; the rest go straight to the footer. Zero cost when there is no quiz.

Persistence: a single already-existing AppSettings table, with distinct prefixes: learn.done.* (existing), concept.done.{module}.{lesson} and concept.done.exercises.{id} (new). There was no schema migration.

Languages: EN and ES fully authored (10 markdown bodies + 11 timelines + 4 exercise briefs + ~66 dictionary keys). FR/DE/CH/JP/AR fall back to EN with the standard lesson-fallback-banner banner per the feedback_translation_pending convention.


Modules out of TT1 scope

The following folders exist in the repository but are not part of the TT1 scope and are not documented in the thesis diagrams. They are kept as placeholders for possible developments in TT2.

Module Status Notes
Simulator/ Empty URP-3D placeholder Standard Unity 6 template. Only own content: SampleScene.unity (camera + directional light) and the template's Readme.cs/ReadmeEditor.cs scripts. No integration with the rest of Kmila (no IPC, no shared loaders, no references in App/Kmila). Reserved for a future 3D visualization (FPGA / circuit).
KmilaFactorySim/ Empty URP-2D placeholder No own C# scripts. Only content: SampleScene.unity (orthographic camera + Global Light 2D). No integration. The original intent ("factory"-style visualization of resource utilization) remains a future exploration.

Practical recommendation: before populating either of the two Unity projects, explicitly define the communication contract with App/Kmila (JSON format over stdio, local WebSocket, shared files, etc.) and add an extended C4 deployment diagram. In the meantime, both projects can be ignored for the purposes of auditing, build, and documentation.


Diagram audit (2026-05-09)

A cross-audit of code and diagrams was performed on 2026-05-09. Result:

  • 8 diagrams rewritten to reflect the current code.
  • 3 diagrams with superficial updates (method/class rename).
  • 19 new diagrams in Documentacion/TT1/diagrams/ (extending the 5.5.x, 5.8.x, 5.10.x, 5.11.x, 5.12.x, 5.13.x series).
  • Captions updated in chapters/capitulo4.tex and chapters/capitulo5.tex.
  • Annex with the \diagramfig{} calls for the 19 new ones in chapters/anexo_diagramas_audit_2026-05-09.tex, ready to integrate at the appropriate narrative place in chapter 5.

Full detail: AUDIT_2026-05-09.md and CHANGELOG_diagrams_2026-05-09.md.


Editor step-replay (`/editor/

After each simulation in the real editor, the new Replay dock tab plays back the captured run through the same ExecutionPlayer used by the /concepts track — read-only Monaco, tape transport, live signal table.

flowchart LR
    Run(["▶ Run"]) --> Builder["ProgramBuilder<br/>RunSimulationAsync"]
    Builder --> Engine["Interpreter<br/>DeltaCycleEngine"]
    Engine --> SH["SignalHistory"]
    SH --> WD["WaveformData<br/>FromSignalHistory"]
    WD --> ReplayB["Services/ReplayBuilder<br/>(static, ≤200 frames)"]
    ReplayB --> Doc["ExecutionTimelineDoc"]
    Doc --> ReplayState["Services/ReplayState<br/>(scoped)"]
    ReplayState -->|OnChange| Panel["Components/StepReplayPanel.razor"]
    Panel --> Player["ExecutionPlayer.razor<br/>(reused from Concepts)"]

    style Builder fill:#f3e5f5,stroke:#7b1fa2
    style Engine fill:#e8f5e9,stroke:#2e7d32
    style ReplayB fill:#e8f5e9,stroke:#2e7d32
    style ReplayState fill:#f3e5f5,stroke:#7b1fa2
    style Panel fill:#e3f2fd,stroke:#1565c0
    style Player fill:#fff3e0,stroke:#e65100

Key design decisions:

Decision Rationale
Reuse ExecutionPlayer instead of a new component It already has a player + tape + signal table + change animation. Zero new UI.
Pure static ReplayBuilder instead of a DI service Converts WaveformDataExecutionTimelineDoc without state.
Cap of 200 frames with downsampling Keeps the tape agile even for long runs. Preserves start + end; the intermediate frames are strided uniformly.
ReplayState scoped instead of a global event-bus Per-circuit: two projects open in two tabs do not step on each other.
HighlightLines always empty in Phase A AST→source-line mapping requires instrumenting Interpreter/DeltaCycleEngine. Phase B; see memory project_editor_live_simulation_playback.

Persistence: none in Phase A — the replay lives in scoped memory, lost on refresh. The SQLite simulation snapshots could be used in a future Phase to reconstruct replays on demand.

Live debugging (backlog #3/#4/#5, 2026-05-27): on top of the same ReplayState + the live DeltaCycleEngine (AttachLive/DetachLive):

Piece What it does
Untrackable-breakpoint notice (#4) RefreshLineMap(source) rebuilds the signal→line map (and the set of condition-lines) from the current code; IsLineTracked/TrackedLines classify each breakpoint. Editor.RepaintBreakpointsAsync() calls the extended JS kmilaSetMonacoBreakpoints(all, untracked, warnMsg) → breakpoints without a signal transition are painted as a hollow, dotted gray glyph + ⚠ tooltip. StepReplayPanel also marks them in its list.
Step-through in the schematic (#3) TrackActiveLines + LiveActiveLines/LiveActiveSignals + OnLiveStepChanged event (throttle ~10 Hz, immediate on pause/step). SchematicViewer in Debug mode pulses the gate whose Cell.SourceLine matches the current delta (is-active-step) and lights up the wires of changed signals; Pause/Resume/Step control bar via Replay.Live*.
Breakpoints on conditions (#5) The parser captures the keyword's line in IfBranch.ConditionLine / CaseWhenBranch.ConditionLine; DeltaCycleEngine.OnConditionEvaluated(line) fires when an if/elsif branch is taken or a case-when arm matches; ReplayState.OnLiveConditionEvaluated pauses if there is a breakpoint on that line — the signal-transition path cannot catch them because the condition changes no signal.
Reattach mid-run (#11) Editor.HydrateWaveformFromLiveEngine() rebuilds _waveformData from Replay.LiveEngine.History when returning to an in-flight sim. Opening a saved run during the active sim activates _liveMergeSuspended (progress/finish merges do not overwrite the saved view); a "Return to the live run" banner rehydrates from the engine.

This closes the empty HighlightLines of the previous row: the engine now does emit the active lines per delta. See memory [kmila-backlog-2026-05-20] items #3/#4/#5/#11.

Full detail of the 2026-05-17 polish pass (6 tasks, all with a green sweep of 281+ tests): CHANGELOG_concepts_module_2026-05-17.md.


WaveformViewer — pipeline and interactive architecture (2026-05-22)

The Waveforms tab of the editor dock renders, for each simulation signal, a step-function trace in inline SVG. The component covers three paths: live visualization during a run, a static snapshot after completion, and a compare mode overlaying multiple snapshots with distinct palettes. The full detail of the 2026-05-22 redesign (closing backlog items #8, #9, #10) lives in CHANGELOG_waveform_overhaul_2026-05-22.md.

flowchart TB
    Sim["Interpreter<br/>DeltaCycleEngine"] --> SH["SignalHistory<br/>(transiciones por señal)"]
    SH --> WD["WaveformData<br/>Signals + TotalTicks + TimescaleNs"]
    WD --> Editor["Pages/Editor.razor<br/>WaveformsContent slot"]
    Snap["SimulationSnapshotStore<br/>(SQLite)"] -.snapshots.-> Editor
    Editor --> WV["Components/WaveformViewer.razor"]

    subgraph WV["Components/WaveformViewer.razor"]
        State["Estado<br/>_zoomWidth, _hiddenSignals, _showClock,<br/>_compareTracks, _hoverId"]
        Render["Render branches<br/>single / compare"]
        Build["BuildTraceSvg / BuildCompareTraceSvg / BuildRulerSvg"]
        ExportSvg["HandleExportSubmitAsync → WaveformExporter.BuildSvg"]
        ExportVcd["BuildVcd<br/>(VCD format)"]
        ExportPng["BuildSvg → kmilaSvgToPngBase64 (canvas)"]
        ExportUml["(no aplica — solo Flow tiene UML)"]
        Reset["ZoomReset async<br/>→ kmilaWaveformFitWidth"]
    end

    State --> Render
    Render --> Build
    Build --> SVG["Inline SVG strips<br/>(step / diamond / sticky ruler)"]

    SVG --> Body[".waveform-viewer__body<br/>overflow:auto, position:relative"]

    Body -. data-view-start-ns,<br/>data-view-end-ns,<br/>data-label-width .-> JS["JS: app.js<br/>kmilaAttachWaveformHover<br/>kmilaWaveformFitWidth"]
    JS -. line + chip .-> Body
    JS -. fit width .-> Reset

    Render --> ExportSvg
    Render --> ExportVcd
    Render --> ExportPng
    Render --> ChipBar["Chip-bar de señales ocultas<br/>(sobre el body)"]

    style WV fill:#f3e5f5,stroke:#7b1fa2
    style Body fill:#fff3e0,stroke:#e65100
    style JS fill:#e8f5e9,stroke:#2e7d32
    style Sim fill:#e8f5e9,stroke:#2e7d32
    style WD fill:#e8f5e9,stroke:#2e7d32
    style Snap fill:#fce4ec,stroke:#c62828

Key design decisions (post-2026-05-22):

Decision Rationale
Inline SVG instead of Chart.js or canvas Each transition of the SignalHistory produces a real step; zoom merely resizes the SVG width without reinitializing a canvas pipeline. Migration inherited from earlier cycles; documented in the component header.
Pan-zoom DETACHED from the waveform body The body already has overflow:auto. Keeping pan-zoom caused two simultaneous pan mechanisms and, after adding per-signal hiding (#9), the cached bounds of clampPan became stale and the user could drag the trace off-screen. Schematic and Flow still use it because they are free canvases. See [project_waveform_no_panzoom].
Bidirectional sticky chrome Left column with names (position: sticky; left: 0) + depth shadow to make the pinning evident; bottom row of the ruler (position: sticky; bottom: 0) so the time axis stays visible when scrolling many signals.
Click-to-hide on each row's label No extra chrome; an eye icon appears on hover. A chip-bar above the body lists the hidden ones for restoring individually or in bulk. Applies to VCD/SVG/PNG exports too.
JS hover cursor instead of Razor @onmousemove @onmousemove routes over SignalR in Blazor Server; a cursor updating at 60 fps collapses the circuit. JS reads data-view-* from the body on each frame, with no round-trips to C#.
Reset = fit-to-width, not a constant 1200 kmilaWaveformFitWidth measures body.clientWidth - 162 and assigns it to _zoomWidth (clamp 200..2.4M). On wide panels it matches the default; on narrow ones it effectively fits the whole trace. No auto-fit on mount: tried and reverted — it compresses dense traces into an illegible solid block.
Zoom ceiling of 2,400,000 px After four rounds of tuning (24k → 96k → 200k → 600k → 2.4M) based on user feedback. Allows ~24 px per cycle in the worst case (100 MHz × 1 ms = 100k cycles). Risk of stutter on iOS Safari when rendering extreme-size SVGs — the correct follow-up if it surfaces as a complaint is path virtualization (emit only the visible range), not lowering the ceiling.
.kmila-dock__panel--waveforms panel with overflow:hidden Same idiom as the Schematic. Without this override, the dock panel added its own vertical scrollbar stacking with the body's (the "third scrollbar" reported by the user).
Filtering on export The three formats (VCD, SVG, PNG) honor _hiddenSignals. For SVG/PNG, a shallowly cloned WaveformData is built (the Transitions remain shared by reference — there is no copy of heavy data).

Associated JS components (Kmila.Shared/wwwroot/js/app.js):

Function Purpose Notes
kmilaAttachWaveformHover(bodySelector) Injects <div class="waveform-hover__line"> + <div class="waveform-hover__chip"> into the body; mousemove / mouseleave / scroll listeners. Reads data-view-start-ns, data-view-end-ns, data-label-width from the body on each update(). Returns an id for detach.
kmilaDetachWaveformHover(id) Removes listeners + created DOM nodes. Called in the component's Dispose().
kmilaWaveformFitWidth(bodySelector, labelW) Returns body.clientWidth - labelW - 2, or -1 if not measurable. Used by ZoomReset.
kmilaSvgToPngBase64(svgString, scale) Rasterizes an SVG to base64 PNG via <canvas> + drawImage. Shared with the rest of the exporters; underwent the tainted-canvas / font-import fix documented in CHANGELOG_v1.19.md (flow PNG item).
kmilaInlineSvgStyles(svgEl) Before exporting: inlines computed styles and removes @import, <foreignObject>, external xlink:href. Mitigates the tainted-canvas bug.
kmilaAttachPanZoom(rootSel, opts) NOT used by WaveformViewer. It is by SchematicViewer + FlowDiagramViewer. In-code documentation of the contain: inline-size + min-width: 0 recipe.

Acknowledged limitations and follow-ups:

  1. Performance at 2.4 M px — desktop OK, iOS Safari potentially slow. Correct fix: virtualized emission of the SVG path to the visible range (viewStartNs..viewEndNs), not lowering the ceiling.
  2. IN/OUT/INOUT quick-filter from backlog #9 — deferred; the click-per-row + chip-bar covers the typical use.
  3. Pinch-zoom on touch — no equivalent to re-attaching pan-zoom. If a real need arises (tablet-first workflow), add a dedicated pinch-only handler that updates _zoomWidth.
  4. Time-range zoom (drag-select over the trace to zoom directly to a range) — would be the natural next step and would obviate the need for high ceilings.

Update 2026-09-13 — scalability on long runs + value readout

Motivation: on runs of the order of seconds (e.g. a 7-segment display that updates every ~50k cycles) the viewer froze the entire app. The cause was not the DOM (the drawing was already windowed), but the algorithmic cost per render on the single thread of the Blazor circuit. Changes (visualization only; the simulation logic remains intact and the VCD/CSV output is byte-identical on runs without overflow):

  • Windowed + decimated access (Phase 1/2). WaveformSignal.GetWindow(startNs, endNs, maxPoints) (in Models/WaveformData.cs) does a binary search (IndexAtOrBefore) of the visible range and returns at most ~1 point per pixel, preserving the first/last and min/max of each bucket so that fast pulses (clock) do not disappear. BuildTraceSvg consumes that window. The hot-spot that copied the full transition list (Transitions.ToArray() + Array.IndexOf) per bus-signal on each render was removed, along with the linear scan from index 0. This closes follow-up #1 (virtualized emission of the path also in the scanning of data, not just the drawing).
  • Trace memoization. TraceSvgCached caches the SVG per signal except on a change of window/zoom/width/color/#transitions. A cursor movement does NOT rebuild the traces — only the value chips —, avoiding the render-storm.
  • MergeFrom O(1) per signal. A Dictionary<string, WaveformSignal> index instead of the FirstOrDefault by name (it was O(signals²) every 200 ms tick).
  • Value column + per-signal toggle. Each row shows the signal's value at the cursor (WaveformData.ValueAt, binary search, last-write-wins — a helper shared with SchematicViewer in Debug mode) and a wave↔value toggle; numeric signals (integer / bus) start in value mode. CSS classes waveform-signal-value, waveform-row__mode-toggle, waveform-row__value-track, waveform-value-readout.
  • Hover cursor. kmilaAttachWaveformHover(body, dotNetRef) now publishes the time under the pointer (throttled ~60 ms) to WaveformViewer.OnCursorHoverSignalCursorBus. SetCursorTick (previously defined but with no callers). Clicking on the ruler still creates time breakpoints. i18n keys: WAVEFORM_VALUE_AT_CURSOR, WAVEFORM_SHOW_AS_VALUE, WAVEFORM_SHOW_AS_WAVE.
  • long timestamp in SignalHistory (Phase 3a). The tick*1000+delta encoding overflowed int past ~2.1 M ticks (~21 ms at 10 ns/tick), corrupting wave/VCD times on long runs. It was widened to long only on the recording/storage path; the attributes path (SignalAttributeHandler.OnDeltaCommit, 'stable(T)/'last_value) keeps its int — the simulation semantics do not change. 167/167 tests green.
  • Discarded: spilling SignalHistory to disk. A transparent spill would require rewriting all read/export paths to be segment-aware (risk to the byte-identical guarantee of VCD/CSV and to replay) and clashes with the append-only watermark of ReadSince. The overflow is already resolved with long; the only case of unbounded RAM (a signal that changes every tick for seconds) already has mitigations (KMILA_SIM_NO_HISTORY=1, hide the clock). It was not implemented because it did not justify the risk.

Update 2026-09-13 — per-bit expandable buses + readable label

Two visualization-only improvements (the generation logic remains intact and the output VCD/CSV is byte-identical; see project_waveform_viz_only):

  • Collapsible std_logic_vector → one wave per bit. The bus is still shown as a single aggregate row (GTKWave lane with a hex label); a chevron (/) in its label expands it into a sub-row per bit (MSB on top, label nombre[n] in downto convention), each drawn by reusing the std_logic branch of BuildTraceSvg over lightweight synthesized signals (BitSignalsFor + BitTraceSvgCached, cached by nombre#bit). To preserve the U/Z/X states per bit —which the aggregate numeric value collapses— a display-only field WaveformTransition.Bits (raw MSB-first string) was added, populated only for buses in FromSignalHistory (static) and MergeFrom (live). That field is read by no export path: Value/Label and the VCD do not change. Expansion state in _expandedVectors (mirror pattern of _hiddenSignals). U/Z/X bits render as the already-existing "unknown" dashed trace, not as 0. CSS classes waveform-row__expand-toggle, waveform-row__expand-spacer, waveform-row--bit, waveform-row__bit-name. i18n keys WAVEFORM_EXPAND_BITS, WAVEFORM_COLLAPSE_BITS.
  • Name↔type label without collision. The direction chip (IN/OUT/INOUT/INTERNAL) was 4px from the mono name, so a name containing "in"/"out"/"internal" read as part of the type. A divider (border-left + padding) was added to the left of the name, and a title with the full name (the name is truncated with ellipsis). The compare mode now also wraps chip+name+width in .waveform-row__label-main to inherit the same divider and the ellipsis containment (it previously lacked both). CSS + markup only.

Other viewers (Schematic, Flow) and the kmilaAttachPanZoom utility

For comparative context:

flowchart LR
    Synth["Interpreter.Synthesis<br/>Netlist + LayeredLayout"] --> SV["Components/SchematicViewer.razor"]
    Flow["Mermaid.js<br/>flowchart compositor"] --> FV["Components/FlowDiagramViewer.razor"]
    WaveData["WaveformData"] --> WVV["Components/WaveformViewer.razor"]

    SV  -. usa .-> PZ["js: kmilaAttachPanZoom<br/>(transform-pan + pinch)"]
    FV  -. usa .-> PZ
    WVV -. NO usa .-> PZ
    WVV -. solo .-> Native["overflow:auto<br/>(scroll nativo)"]

    style SV fill:#f3e5f5,stroke:#7b1fa2
    style FV fill:#f3e5f5,stroke:#7b1fa2
    style WVV fill:#f3e5f5,stroke:#7b1fa2
    style PZ fill:#e8f5e9,stroke:#2e7d32
    style Native fill:#fff3e0,stroke:#e65100
Viewer Structure Gestures Rationale
SchematicViewer Free-form (gates + wires at arbitrary X/Y positions) Pan + pinch (kmilaAttachPanZoom) Without a rectilinear layout, native scroll is not enough: the user wants to center the view, zoom contextually, chase a long wire.
FlowDiagramViewer Free-form (Mermaid flowchart) Pan + pinch (kmilaAttachPanZoom) Same rationale — the graph emitted by Mermaid can be arbitrarily wide/tall and does not benefit from linear scroll.
WaveformViewer Row-structured (rows aligned by time) Native scroll + toolbar buttons Every row has the same height and shares the X axis. Free 2D pan adds nothing over scroll; the Zoom-In/Out/Reset toolbar covers the change in horizontal aspect ratio.

Detailed per-module diagrams (2026-05-22)

This section expands the high-level diagram by showing the main services of each module and the data flow between them at the class level. Intended for auditing — the more granular details (each handler, helper, etc.) live in each module's README.md.

Parser

flowchart LR
    Source["Fuente VHDL<br/>(string)"] --> Tk["Tokenizer<br/>+ TokenizeLineAt (v1.15)"]
    Tk --> Tokens["Token[]<br/>(con índice de línea original)"]
    Tokens --> PP["PortsParser<br/>(v1.17: Levenshtein typo-suggest;<br/>v1.15: catch missing semicolons)"]
    Tokens --> EP["EntityParser"]
    Tokens --> AP["ArchitectureParser<br/>(v1.15: procedure decls,<br/>direct entity instantiation,<br/>arithmetic generic defaults)"]
    Tokens --> ImpP["ImportParser<br/>(library / use)"]
    AP --> AST["AST<br/>(Architecture, Process,<br/>ConcurrentStatement, ...)"]
    PP --> AST
    EP --> AST
    ImpP --> AST
    PP -.diagnostics.-> DL["SkipDiagnosticLog<br/>(códigos VHD-*)"]
    AP -.diagnostics.-> DL
    EP -.diagnostics.-> DL

    style Tk fill:#e8f5e9,stroke:#2e7d32
    style PP fill:#e8f5e9,stroke:#2e7d32
    style EP fill:#e8f5e9,stroke:#2e7d32
    style AP fill:#e8f5e9,stroke:#2e7d32
    style ImpP fill:#e8f5e9,stroke:#2e7d32
    style AST fill:#fff3e0,stroke:#e65100
    style DL fill:#fce4ec,stroke:#c62828

Surfaces: Parser/Models/AST.cs, Parser/Services/Tokenizer.cs, Parser/Services/PortsParser.cs, Parser/Services/ArchitectureParser.cs, Parser/Services/EntityParser.cs, Parser/Services/ImportParser.cs. Parser/Program.cs runs the suite of 40 tests and reports an exit-code for CI.

Interpreter — runtime + delta-cycle

flowchart TB
    AST["AST del Parser"] --> Ar["Architecture<br/>(handler central)"]
    Pkg["PackageRegistry<br/>(singleton)"] --> Ar
    Ent["EntityRegistry<br/>(singleton)"] --> Ar
    Fn["FunctionRegistry<br/>(v1.15)"] --> Ar
    IEEEL["IEEELibraryLoader<br/>(v1.15 #20, delegated to LibraryCompiler en v1.16 #22-#26)"] --> Pkg

    Ar --> SR["SimulationRunner<br/>(coordinador)"]
    SR --> DCE["DeltaCycleEngine<br/>(IDisposable)"]
    DCE --> PS["ProcessScheduler<br/>(activación por sensitivity list)"]
    DCE --> SS["SignalScheduler<br/>(after clause, signal queue)"]
    DCE --> SAH["SignalAttributeHandler<br/>('event / 'last_value /<br/>'stable / 'active)"]
    DCE --> SH["SignalHistory<br/>(transiciones por señal)"]
    DCE --> SDL["SkipDiagnosticLog<br/>(VHD-* per omisión)"]

    SH --> WD["WaveformData.FromSignalHistory"]
    SH --> VCD["VCD / CSV / Text export<br/>(en Interpreter; el frontend wrapper<br/>está en WaveformViewer.BuildVcd)"]

    DCE --> EV["Eventos live-replay<br/>OnFrameEmitted (delta + líneas)<br/>OnConditionEvaluated (if/elsif/when, #5)<br/>Pause/Resume/StepOneDelta"]
    EV --> RS["Kmila.Shared/ReplayState<br/>(breakpoints, schematic step-through)"]

    style Ar fill:#e8f5e9,stroke:#2e7d32
    style DCE fill:#e8f5e9,stroke:#2e7d32
    style PS fill:#e8f5e9,stroke:#2e7d32
    style SS fill:#e8f5e9,stroke:#2e7d32
    style SAH fill:#e8f5e9,stroke:#2e7d32
    style SH fill:#fff3e0,stroke:#e65100
    style WD fill:#fff3e0,stroke:#e65100

Tests: the Interpreter has an xUnit suite in Interpreter/Tests/ (run with dotnet test), which at the close of the Tier 4.3 arc (2026-07-30) stood at ~167 cases. The IEEE/synthesis pipeline subset is formed by DeltaCycleEngineTests + SynthesisTests + IEEELibraryLoaderTests + LibraryCompilerTests + IEEEPrimitivesTests + IEEELoweringEndToEndTests + NestedIEEECallTests. There is also the smoke corpus test*.vhdl (54 fixtures) that Program.cs runs. Detail and per-surface status in ../Interpreter/README.md §7. (The "82 cases / --test-delta" figure from earlier revisions was the IEEE subset of v1.19.)

Execution modes: Visual vs Fast + timestamp fix >21 ms (2026-09-13)

Motivation: runs of seconds of simulated time (≥1 s at 10 ns/tick = 10⁸ ticks) took too long in the Editor. A Fast/Visual selector was added to the Run-bar (SimulationParameters.FastMode, RunBar toggle → read by Editor.OnRunRequested):

  • Visual (default): the reference SimulationRunner (tick++ loop, visits every tick) + live waveform (200 ms timer in ProgramBuilder) + per-delta replay (ReplayState.AttachLive → breakpoints/step/active-line). No behavior changes.

  • Fast: two combined accelerations, visualization/orchestration only (the simulation output is identical):

    1. Fewer visual effects: ProgramBuilder.RunSimulationAsync(fastRun:true) omits the 200 ms timer (there is no history merge nor per-frame SVG redraw) and Editor omits Replay.AttachLive. A cheap progress bar remains (OnProgressChanged); the full waveform is built once at the end (OnSimulationDataReadyFromSignalHistory). Pause/Stop keep working (they go through the runner, not through ReplayState); breakpoints/step do not.
    2. "Duplicated" event-driven engine: new Interpreter/Services/FastSimulationRunner.cs — a sibling of SimulationRunner that drives the same DeltaCycleEngine, but skips idle ticks: it advances to min( next clock edge, next stimulus, SignalScheduler.NextScheduledTick, DeltaCycleEngine.NextProcessWakeTick, final stamp ). It is correct because both engine queues drain everything due at <= currentTick (SignalScheduler.AdvanceTime, DrainReadyProcessWakesAsync), and an idle tick produces no delta → neither a history nor a signal change. The engine and SimulationRunner remain intact; the only addition to the engine is the read-only getter DeltaCycleEngine.NextProcessWakeTick (mirror of NextScheduledTick, no behavior). Both runners implement ISimulationRunner so ProgramBuilder picks one at runtime.

    Byte-identical equivalence verified by FastRunnerEquivalenceTests (Fast's VCD == reference VCD for clock, edge-triggered flip-flop, and wait for). Speed probe: 50,000 ticks with a slow clock (1 MHz @ 10 ns) → reference ~97 ms vs Fast ~7 ms (~14×), identical output. Note: if the clock toggles every tick (e.g. 50 MHz @ 10 ns), there are no idle ticks to skip and Fast's gain is only that of "fewer visual effects"; to lower the floor of 10⁸ ticks there, the lever is a coarser tick resolution (changes granularity, separately). Minor pending: persist the mode across sessions (today it is session state).

  • intlong timestamp fix (>21 ms), correctness — authorized by the user. The time stamp of the attributes path was still int ((int)(_currentTick*1000)), which overflows Int32.MaxValue past tick ≈ 2.147×10⁶ (~21.5 ms at 10 ns/tick), corrupting 'stable/'last_value/'event/'delayed and the clock/stimulus transition timestamps. It was widened to long: SignalAttributeHandler._lastChangeMoments / _currentTimeMoment / OnDeltaCommit / StampStimulusChanges / GetStable, the timeMoment in DeltaCycleEngine.RunDeltaCycleAsync, and the two (int)(tick*1000) casts in SimulationRunner. It changes the output only for runs >21 ms (from incorrect to correct); <21 ms stays identical. Suite: 170/170 green (167 previous + 3 equivalence).

  • Execution ETA + performance log (2026-09-13). SimulationParameters estimates the remaining time live (RemainingSeconds/ElapsedSeconds) from the observed rate of progress (remaining = elapsedActive × (100−%)/%, excluding paused time; it abstains until ≥1 % and ≥0.3 s so as not to show absurd numbers). The Run-bar shows it under the progress bar ("~5s remaining" / "estimating…" / "paused"). There is no reliable prior prediction (the cost per tick varies by design), so it is calibrated on the fly. ProgramBuilder also emits a [Sim.Perf] line to /logs (mode, elapsed, totalTicks, and in Fast visitedTicks

    • % of executed ticks) to diagnose why a run was slow. Tests in SimulationEtaTests; suite 172/172.

Interpreter — synthesis path ("FPGA visualization" mode)

flowchart LR
    AST["AST del Parser"] --> Synth["Synthesizer<br/>(orquestador)"]
    Synth --> CAS["ConcurrentAssignSynthesizer<br/>(v1.19: nested DF recursion)"]
    Synth --> PrS["ProcessSynthesizer"]
    Synth --> IPL["IEEEPrimitives.TryLower<br/>(unsigned/signed/to_integer/<br/>resize/conv_std_logic_vector → cell)"]

    CAS --> NL["Netlist<br/>(cells + nets + ports)"]
    PrS --> NL
    IPL --> NL

    NL --> LL["LayeredLayout<br/>(posiciona cells)"]
    LL --> SVTab[".kmila-dock--schematic<br/>(Components/SchematicViewer.razor)"]

    style Synth fill:#e8f5e9,stroke:#2e7d32
    style CAS fill:#e8f5e9,stroke:#2e7d32
    style PrS fill:#e8f5e9,stroke:#2e7d32
    style IPL fill:#e8f5e9,stroke:#2e7d32
    style NL fill:#fff3e0,stroke:#e65100
    style LL fill:#fff3e0,stroke:#e65100
    style SVTab fill:#f3e5f5,stroke:#7b1fa2

ConcurrentAssignSynthesizer.ResolveDeferredArgs gains in v1.19 (#29) the DeferredFunction nested case that recursively lowers nested IEEE calls (to_integer(unsigned(addr))) into a correctly-wired graph of cells.

Debugger

flowchart LR
    UI["UI (Editor.razor)<br/>RunBar → ProgramBuilder"] --> Dbg["Debugger.Run<br/>(orquestador)"]
    Dbg --> P["Parser.ParseAsync"]
    P --> Dbg
    Dbg --> Diag["RunDiagnostics<br/>(propaga aún si el parse falla)"]
    Diag --> SDL2["SkipDiagnosticLog<br/>+ Diagnostics<br/>(devueltos al UI)"]
    Dbg --> I["Interpreter.SimulationRunner"]
    I --> SH2["SignalHistory + WaveformData"]
    SH2 --> Dbg
    Dbg --> UI2["UI (Waveforms, Schematic,<br/>Output, Replay)"]

    style Dbg fill:#e8f5e9,stroke:#2e7d32
    style Diag fill:#e8f5e9,stroke:#2e7d32
    style UI fill:#e3f2fd,stroke:#1565c0
    style UI2 fill:#e3f2fd,stroke:#1565c0

Debugger.RunDiagnostics is the contract that makes the Output dock tab always have useful content, even when a Parser exception knocked down the structural tree.

LibraryCompiler

flowchart TB
    UI["UI / Editor"] --> LCF["LibraryCompiler<br/>(Facade)"]
    LCF --> PCx["ProjectContext"]
    LCF --> Res["LibraryResolver<br/>(topo-sort de deps)"]
    LCF --> Orch["BuildOrchestrator"]

    Orch --> Lin["LinearScheduler"]
    Orch --> Par["ParallelScheduler"]
    Orch --> CacheStrat{"Strategy:<br/>OnDemand /<br/>Cached /<br/>Persistent"}

    Orch --> Bld["LibraryBuilder<br/>(una librería)"]
    Bld --> CL["CompiledLibrary"]
    Bld -.usa.-> Stubs["Builtins/IEEE/<br/>std_logic_1164.vhd<br/>numeric_std.vhd<br/>(embedded)"]

    CacheStrat -. persiste .-> IBS["IBlobStore<br/>(contrato; concretos en host)"]
    CL --> FR["FunctionRegistry<br/>(sembrado por LCF)"]

    style LCF fill:#e8f5e9,stroke:#2e7d32
    style Orch fill:#e8f5e9,stroke:#2e7d32
    style Bld fill:#e8f5e9,stroke:#2e7d32
    style CL fill:#fff3e0,stroke:#e65100
    style FR fill:#fff3e0,stroke:#e65100

Details: IEEE_PIPELINE_GUIDE.md and CHANGELOG_v1.16.md.

Export pipeline (cross-cutting)

flowchart LR
    Sim["WaveformData /<br/>Netlist /<br/>FlowAST"] --> Exporters{Tipo de export}

    Exporters --> Vcd["BuildVcd<br/>(en WaveformViewer)<br/>→ texto VCD"]
    Exporters --> WaveSvg["WaveformExporter.BuildSvg<br/>+ kmilaSvgToPngBase64<br/>→ SVG / PNG"]
    Exporters --> SchSvg["SchematicViewer.BuildSvg<br/>+ canvas raster<br/>→ SVG / PNG"]
    Exporters --> FlowSvg["FlowDiagramViewer.BuildSvg<br/>+ kmilaInlineSvgStyles<br/>+ canvas raster<br/>→ SVG / PNG"]
    Exporters --> FlowUml["PlantUmlFlowEmitter<br/>(v1.19, backlog #1)<br/>→ .puml texto"]

    Vcd --> Save["IFileExportService.SaveTextAsync /<br/>SaveBytesAsync"]
    WaveSvg --> Save
    SchSvg --> Save
    FlowSvg --> Save
    FlowUml --> Save
    Save --> Disk["Documents/Kmila/<br/>SVG/ PNG/ VCD/ UML/"]

    style Exporters fill:#fff3e0,stroke:#e65100
    style Save fill:#e3f2fd,stroke:#1565c0
    style Disk fill:#fce4ec,stroke:#c62828

Operational notes:

  • The 64 MB cap of Blazor Server's SignalR (see [project_blazor_signalr_message_size]) limits the size of the bytes that can travel JSInterop → C# in a single round-trip. Extremely large diagrams (10k+ Mermaid Flow nodes) could require a direct trigger in JS (Blob+anchor) instead of the current round-trip.
  • kmilaInlineSvgStyles is the fix for the tainted-canvas bug reported in v1.19 (CHANGELOG entry "Flow PNG / SVG export broken"). It sanitizes @import, <foreignObject>, external <image>, xlink:href before the drawImage that rasterizes.

UI surface — Editor dock tabs

flowchart LR
    Editor["Pages/Editor.razor"] --> ED["EditorDock.razor"]
    ED --> Tabs{"DockState.Active"}

    Tabs -- Entities --> Tab1["Lista de entidades parseadas"]
    Tabs -- Schematic --> Tab2["SchematicViewer<br/>(pan-zoom JS)"]
    Tabs -- Flow --> Tab3["FlowDiagramViewer<br/>(pan-zoom JS)"]
    Tabs -- Ports --> Tab4["PortsPanel<br/>(estímulos por puerto)"]
    Tabs -- Waveforms --> Tab5["WaveformViewer<br/>(scroll nativo + hover)"]
    Tabs -- Runs --> Tab6["RunsHistoryPanel<br/>(SQLite snapshots)"]
    Tabs -- Files --> Tab7["FilesCard"]
    Tabs -- Output --> Tab8["BuildLog + diagnostics"]
    Tabs -- Replay --> Tab9["StepReplayPanel<br/>→ ExecutionPlayer"]

    style Tab2 fill:#f3e5f5,stroke:#7b1fa2
    style Tab3 fill:#f3e5f5,stroke:#7b1fa2
    style Tab5 fill:#f3e5f5,stroke:#7b1fa2
    style Tab9 fill:#f3e5f5,stroke:#7b1fa2

Each tab is a named RenderFragment param of EditorDock. The active tab is persisted per project in EditorDockState + localStorage. The Replay tab appeared in the 2026-05-17 polish pass; Flow appeared with the UML export 2026-05-21 (see [project_flow_uml_export]).


Visual Runtime — canvas-driven infinite-run mode (Phase 0, 2026-06-24; Phase 3+ post-2026-06-26)

State as of 2026-07-24: the Phase 0–2 blocks documented below are the base; the runtime today has the full set of 12 widgets + bit-slice bindings + bounce + 4 example gallery cases (Phase 3), plus canvas record + export + locator + pan-wall (Phase 3+). See VISUAL_MODE.md §10-12 for the current state of the widgets and CHANGELOG.md "Visual Runtime — full set + slices …" for the shipping detail.

A simulation mode complementary to the bounded run: it binds the design's ports to virtual components (LED, button) on a canvas and lets the student interact. It reuses 100% of the existing simulation engine — there is no second DeltaCycleEngine.

flowchart LR
    UI["Pages/Visual.razor"] --> Coord["VisualRunCoordinator"]
    Coord --> Engine["DeltaCycleEngine<br/>(reused)"]
    Engine --> Frame["OnFrameEmitted"]
    Frame --> Ring["FrameRing<br/>(2000 ticks, bounded)"]
    Frame --> Coord

    Btn["ButtonWidget"] -- pointerdown/up --> Map["InputStateMap"]
    Coord -- "drain between ticks" --> Map
    Map -- "Variable.Value=" --> Engine
    Engine -- "Variable.OnValueChange" --> Handler["SignalAttributeHandler"]
    Handler -- "_activeFlags[name]=true" --> Engine

    Coord --> Ring
    Ring --> Led["LedWidget<br/>(reads latest port value)"]

    Persist["KvizSerializer (.kviz JSON)"] <--> UI

    style Engine fill:#fff3e0,stroke:#e65100
    style Handler fill:#fff3e0,stroke:#e65100
    style Coord fill:#e3f2fd,stroke:#0d47a1
    style Map fill:#e3f2fd,stroke:#0d47a1
    style Ring fill:#e3f2fd,stroke:#0d47a1

Pieces in orange = preexisting in the simulator core (not touched). Pieces in blue = new in Kmila.Shared/Services/VisualRun/ and Kmila.Shared/Components/VisualRun/.

The critical seam is the coordinator.LoopAsync cycle → writes ports → engine.AdvanceTime(tick)engine.RunTimeStepAsync() → snapshot. For details of each component's contract, the supported ports, the .kviz schema and the phase 1+ roadmap, see VISUAL_MODE.md. The original plan and the blocked decisions live in HANDOFF_visual_runtime_2026-06-24.md.

Phase 1A/B (2026-06-25): two new components (SwitchWidget, LedArrayWidget) and an auto-clock inside the coordinator. Any IN port whose name reads like a clock (clk, clock, aclk, …) is toggled every CLOCK_HALF_PERIOD_TICKS = 8 ticks — unless the user has wired a Switch/Button to that port. Visual.razor collects the user-managed ports and passes them as userDrivenPorts to the coordinator's constructor, which excludes them from its internal _clockPorts list. This lets the counter4 fixture count without external stimulus, while a design with a manual clk still works.

Phase 2 (2026-06-25): the five deliverables of HANDOFF §8 are closed in a single session:

  1. Speed slider + custom ms/tick — the coordinator accepts an override CustomTickDelayMs: int?; when it is null it falls back to the preset Speed.TickDelayMs(). The page maps an <input type="range"> 0..3 to the VisualSpeed enum and an <input type="number"> to the override.
  2. Pause / Continuecoordinator.Pause() / Resume() with a loop that sleeps 20 ms between checks while IsPaused.
  3. Tick breakpointsTickBreakpoints: HashSet<ulong> consulted at the start of each tick; on a match the loop pauses, fires OnBreakpointHit(tick) and arms the sentinel _lastBreakTick so that Resume() does not break again on the same tick.
  4. Frame scrubbingFrameRing.Nearest(tick) finds the frame nearest within the ring window. While paused, Pages/Visual.razor.ApplyScrubFrame() rewrites port.Value for each port of the entity from frame.PortValues; the widgets re-read and redraw. Resume() lets the next tick overwrite everything.
  5. StepperWidget — fourth interactive widget. It decodes 4 pins phaseA/B/Ān/B̄n against the canonical table of 8 Gray patterns; it computes the delta WrappedDelta(prevIdx, newIdx) to accumulate steps with correct wrap-around. The rotor (transform: rotate(...)) turns 360 / stepsPerRev per step.

At the persistence level the setting KMILA_VISUAL_RING_BUFFER (256..16384, default 2000) is added to AppSettings. Visual.razor reads it in OnInitialized and takes Math.Max(perDoc, settings) to size the coordinator's FrameRing.

7-seg — decoding modes + polarity (2026-09-14): the 7-segment bank (SevenSegmentBankWidget) gains the same mode as the standalone digit — raw (packed segment bus, width digits×7), hex and dec (packed nibble bus, width digits×4, decoded internally). Both widgets also gain a polarity config: cathode (default, active-high, previous behavior) or anode (active-low, inverts the read bit). The polarity applies only in raw mode; in decoded modes the widget chooses which segments light up. The pattern table and the decode are extracted to Services/VisualRun/SevenSegPatterns.cs (reused between both widgets). It is a visualization-only change: it does not touch the engine nor the simulation output, and existing .kviz files render the same (defaults raw/cathode). No schema changes (Config is an open dictionary) nor pin changes (KvizWidgetCatalog). Per-widget contracts in VISUAL_MODE.md §5.

Buzzer — audio cut-off on stop (2026-09-14): the BuzzerWidget emitted the tone from OnAfterRenderAsync based on the port values, which freeze on stopping or pausing the simulation (including the auto-pause on tab change) — the Web Audio oscillator kept sounding after stopping. Fixed by tying the audible state to the execution cycle: IsActive now requires Coordinator { IsRunning: true, IsPaused: false } (CoordinatorActive), so that the next render after StopRun/Pause pushes silence. In addition, VisualPanel calls kmilaAudio.stopAll() (new in kmila-audio.js) on stop and on hiding the tab, as an immediate cut-off. Visualization/audio only; no engine changes.

7-seg bank — multiplexed mode (2026-09-14): the bank gains a fourth mode = "mux" that models a real multiplexed display: all digits share the segment lines (a..g) and each digit has its own common / select line (anode/cathode) that the design strobes one at a time. The wiring (config, mux only) offers both wiring styles requested by the user: packed (two vectors seg + sel, assumed order, compact) or explicit (individual pins a..g + an0..an{digits-1}, each wireable separately). selectActiveLow (default true) sets whether the digit is enabled with 0 (the an convention of the boards) or with 1. Since at simulation speed only one digit is selected per tick, with persistence (default true) the widget retains per digit the last pattern seen while its selection was active, walking backwards through the FrameRing (VisualRunCoordinator.Frames) — with no new state in the engine nor changes in DeltaCycleEngine. polarity now applies also to the segments in mux. KvizWidgetCatalog.PinsOf becomes mode/wiring-dependent for the bank, and OnSevenSegBankModeChange/DigitsChange/WiringChange call Sanitize to prune obsolete bindings when the pin set changes. It remains visualization only; previous .kviz files render the same (default raw).


Last updated: May 2, 2026 (module data); May 9, 2026 (diagram audit); May 17, 2026 (polish pass + step-replay); May 22, 2026 (WaveformViewer overhaul + detailed per-module diagrams); June 24, 2026 (Visual Runtime Phase 0 seam); June 25, 2026 (Phase 1A/B Switch+LED array + Phase 2 speed/scrub/bp/stepper); September 13, 2026 (WaveformViewer: windowing+decimation by zoom, value column + per-signal toggle, hover cursor, long timestamp in SignalHistory); September 14, 2026 (7-seg bank: raw/hex/dec modes + cathode/anode polarity in both 7-segment widgets; multiplexed mux mode with packed/explicit wiring, select level and vision persistence; fix for the buzzer that kept sounding after stopping/hiding the tab).