Arquitectura de Kmila-9s
Aplicación Multiplataforma para el Aprendizaje y Depuración de Código VHDL sin Requerimiento de Hardware Físico
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
| Módulo | Descripción |
|---|---|
| App | Interfaz multiplataforma (MAUI/Blazor) con editor Monaco, gestión de proyectos, i18n (7 idiomas), y validación de recursos FPGA |
| Debugger | Capa de pre-procesamiento (no orquestador). Lee un .vhdl, corre un lint (RunDiagnostics → VHD-MISSING-SEMICOLON, VHD-BLOCK-BALANCE) y un pase estructural que hidrata tipos Interpreter (Entity/Architecture). No ejecuta la simulación ni llama a TimeMachine; el flujo de simulación en vivo lo maneja Kmila.Shared.Services contra el Interpreter. Propaga índices de línea al Tokenizer (TokenizeLineAt) y publica Diagnostics aun cuando el parse estructural lance excepción. (Correcto desde v1.14.1: revisiones previas describían un rol de "orquestador central / State Manager / Time Travel" que nunca existió — ver ../Debugger/README.md.) |
| Parser | Analiza el código VHDL: análisis léxico (Tokenizer), sintáctico, y generación del Árbol de Sintaxis Abstracta (AST). En v1.15 acepta declaraciones de procedure en el encabezado de la arquitectura, instanciación directa de entidad (label : entity work.X(arch)), y expresiones aritméticas como valores por defecto de generic. |
| Interpreter | Ejecuta la lógica VHDL con algoritmo Shunting-yard, ciclos delta, y gestión de estados de señales. v1.15 introduce FunctionRegistry (firmas de funciones y procedimientos como cajas negras), SkipDiagnosticLog (códigos VHD-* para todo construct silenciosamente omitido), e IEEELibraryLoader que sembra los nombres de IEEE.std_logic_1164 y IEEE.numeric_std (delegado al módulo LibraryCompiler desde v1.16 #22-#26). Soporta case <slice>, <sig>'range en for-loops, y alias con tamaño efectivo. Motor Tier 4.1–4.3 (cerrado 2026-07-28): atributos de señal 'event/'last_value/'stable[(T)]/'active (SignalAttributeHandler + DeferredAttribute); case?/is? (matching-case VHDL-2008); body-synthesis MVP de funciones/procedimientos (inlining single-return + cascada if/elsif/else → MUX, con frames ProcedureFrame/LoopFrame/BranchFrame/IProcessFrame y fallback UserFunctionBlackBox). Dos runners intercambiables vía ISimulationRunner: SimulationRunner (Visual/Slow) y FastSimulationRunner (Fast, salta ticks ociosos, byte-idéntico). |
| TimeMachine | Controla el tiempo de simulación discreto (10 ns/tick), ciclos de reloj, y proporciona control de ejecución (inicio/pausa/parada). v1.15 sustituye el REPL interactivo de Program.cs por un test-runner automatizado de 23 casos (Clock, TimeClock, Timer + 3 stress). |
Plataformas: Windows, macOS, Linux, Android, iOS | Stack: .NET 9, MAUI, Blazor | Base de Datos: SQLite
Perfil de memoria (baseline v1.15, RSS pico mediano — no re-medido desde entonces): Parser 94 MB · Interpreter 101 MB · Interpreter --test-delta 56 MB · Debugger 98 MB · TimeMachine 37 MB. Todos los .csproj ejecutables declaran ServerGarbageCollection=false para favorecer Workstation GC en dispositivos de bajo RAM. (Cifras de referencia v1.15; el motor creció con Tier 4.1–4.3, así que trátalas como orden de magnitud, no como valores actuales.)
Notas de versión: los CHANGELOG_v1.15.md … CHANGELOG_v1.19.md y los CHANGELOG_*_2026-*.md por-tema ahora viven en _archive/changelogs/ y están fusionados en el CHANGELOG.md consolidado (fuente más fresca). El trabajo posterior a julio se resume en STATUS.md §0.
Sistema de botones — slot grammar (2026-06-28). Tres familias de botones (.kmila-btn Family 11 · .kmila-btn-icon Family 14 · .kmila-btn-tab Family 15) viven en Kmila.Shared/wwwroot/css/buttons.css. Encima de ellas se apoya una gramática de slots que dicta dónde va cada botón, qué tamaño lleva para su rol, y cómo se agrupa con sus vecinos:
.kmila-slot-input-trailing— input absorbe ancho, botón comparte la fila aalign-items: stretch; nunca cae debajo..kmila-slot-banner— texto + acción al final del banner; texto envuelve, botón nunca envuelve..kmila-slot-modal-footer— fila de acciones alineada a la derecha al pie del modal; ≤520 px apila invertido para alcance del pulgar..kmila-slot-header-tools(+__filter,__filter-divider,__actions) — filter shelf (search ± select integrados en un contenedor de 44 px) + cluster de acciones separado. Reflow a dos filas en ≤768 px..kmila-slot-segmented— pill background unifica una fila de.kmila-btn-tabpara que se lea como un solo segmented control..kmila-slot-toolbar-divider/.waveform-viewer__divider— separador vertical fino entre clusters dentro de la misma toolbar.
Reglas no codificadas como clases (variant rationalization, swap Family 11 → 14 en filas de lista, retirar .btn-lg huérfano, single-family-per-toolbar) se documentan en CHANGELOG_button_placement_2026-06-28.md y en la propuesta congelada en Documentacion/UI_AUDIT_2026-06-28-button-placement/SUMMARY.md.
Mini-currículo de Conceptos (/concepts, 2026-05-17)
Pista paralela a /learn y /practice, dedicada al modelo de ejecución detrás de VHDL: secuencial dentro de un proceso, concurrente en el cuerpo de la arquitectura, y por qué el paralelismo real solo existe en hardware. Documentación canónica en ROADMAP_concurrent_execution_module.md; detalles de implementación en 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
Decisiones de diseño clave:
| Decisión | Razón |
|---|---|
Catálogo y store clonados de LessonCatalog/LessonProgressStore en lugar de parametrizar |
Isolation: la pista de Conceptos puede evolucionar su esquema sin riesgo de regresión en /learn. Mismo costo de código, mucho menor blast radius. |
| Timelines pre-grabados en JSON (Phase 1) en vez de instrumentar el simulador | Permite que cada paso lleve una nota didáctica y se controle el ritmo narrativo; el simulador no necesita cambios. Phase 2 del roadmap puede sustituir con emisión en vivo sin tocar el ExecutionPlayer. |
kmila-blocks (JS engine bespoke, wwwroot/js/kmila-blocks*.js) reemplaza Blockly desde v1.20 |
Un solo kmila-blocks.js (~36 KB) sustituyó el bundle Blockly (~1 043 KB). Los nombres de interop kmilaBlockly* se preservan por compatibilidad histórica; el engine es kmila-blocks, no Blockly. Ver [[project_blockly_replacement_plan]]. |
| Fingerprint canónico (workspace JSON → string ordenado) en lugar de comparación estructural | Permite catálogos pre-armados pequeños y un mensaje amigable "esta combinación aún no está preparada" cuando el alumno construye algo fuera del set. Es ordenamiento-sensible — variantes conmutativas requieren entradas adicionales en el catálogo. |
Quizlets como [Parameter] opcional en Concept |
Tres lecciones (1, 3, 5) llevan validación gate; el resto sale al footer directamente. Cero costo cuando no hay quiz. |
Persistencia: una sola tabla AppSettings ya existente, con prefijos distintos: learn.done.* (existente), concept.done.{module}.{lesson} y concept.done.exercises.{id} (nuevos). No hubo migración de esquema.
Idiomas: EN y ES totalmente autorados (10 markdown bodies + 11 timelines + 4 exercises briefs + ~66 keys de diccionario). FR/DE/CH/JP/AR caen al EN con el banner estándar lesson-fallback-banner según la convención de feedback_translation_pending.
Módulos fuera del alcance de TT1
Las siguientes carpetas existen en el repositorio pero no son parte del alcance de TT1 y no están documentadas en los diagramas de tesis. Se mantienen como placeholders para posibles desarrollos en TT2.
| Módulo | Estado | Notas |
|---|---|---|
| Simulator/ | Placeholder URP-3D vacío | Plantilla Unity 6 estándar. Único contenido propio: SampleScene.unity (cámara + luz direccional) y los scripts Readme.cs/ReadmeEditor.cs del template. Sin integración con el resto de Kmila (sin IPC, sin loaders compartidos, sin referencias en App/Kmila). Reservado para una futura visualización 3D (FPGA / circuito). |
| KmilaFactorySim/ | Placeholder URP-2D vacío | Sin scripts C# propios. Único contenido: SampleScene.unity (cámara ortográfica + Global Light 2D). Sin integración. La intención original (visualización tipo "factory" de utilización de recursos) queda como exploración futura. |
Recomendación práctica: antes de poblar cualquiera de los dos proyectos Unity, definir explícitamente el contrato de comunicación con
App/Kmila(formato JSON sobre stdio, WebSocket local, archivos compartidos, etc.) y agregar un diagrama C4 de despliegue extendido. Mientras tanto, ambos proyectos pueden ignorarse para efectos de auditoría, build y documentación.
Auditoría de diagramas (2026-05-09)
Una auditoría cruzada de código y diagramas se realizó el 2026-05-09. Resultado:
- 8 diagramas reescritos para reflejar el código actual.
- 3 diagramas con actualizaciones superficiales (renombre de método/clase).
- 19 diagramas nuevos en
Documentacion/TT1/diagrams/(extendiendo las series 5.5.x, 5.8.x, 5.10.x, 5.11.x, 5.12.x, 5.13.x). - Captions actualizadas en
chapters/capitulo4.texychapters/capitulo5.tex. - Anexo con las llamadas
\diagramfig{}para los 19 nuevos enchapters/anexo_diagramas_audit_2026-05-09.tex, listo para integrar en el lugar narrativo apropiado de capítulo 5.
Detalle completo: AUDIT_2026-05-09.md y CHANGELOG_diagrams_2026-05-09.md.
Editor step-replay (`/editor/
Después de cada simulación en el editor real, la nueva pestaña Replay del dock reproduce el run capturado a través del mismo ExecutionPlayer que usa la pista /concepts — Monaco de solo lectura, transporte de cinta, tabla de señales en vivo.
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
Decisiones de diseño clave:
| Decisión | Razón |
|---|---|
Reutilizar ExecutionPlayer en lugar de un nuevo componente |
Ya tiene reproductor + tape + tabla de señales + animación de cambios. Cero UI nueva. |
ReplayBuilder puro estático en lugar de servicio DI |
Convierte WaveformData → ExecutionTimelineDoc sin estado. |
| Cap de 200 frames con downsampling | Mantiene el tape ágil incluso para runs largos. Preserva inicio + fin, los frames intermedios se estridan uniformemente. |
ReplayState scoped en lugar de event-bus global |
Por-circuito: dos proyectos abiertos en dos pestañas no se pisan. |
HighlightLines siempre vacío en Phase A |
Mapeo AST→línea-fuente requiere instrumentar Interpreter/DeltaCycleEngine. Phase B; ver memoria project_editor_live_simulation_playback. |
Persistencia: ninguna en Phase A — el replay vive en memoria scoped, perdido al refresh. Las snapshots de simulación en SQLite podrían usarse en una Phase futura para reconstruir replays a demanda.
Depuración en vivo (backlog #3/#4/#5, 2026-05-27): sobre el mismo ReplayState + el DeltaCycleEngine en vivo (AttachLive/DetachLive):
| Pieza | Qué hace |
|---|---|
| Aviso de breakpoint no rastreable (#4) | RefreshLineMap(source) reconstruye el mapa señal→línea (y el conjunto de líneas-condición) desde el código actual; IsLineTracked/TrackedLines clasifican cada breakpoint. Editor.RepaintBreakpointsAsync() llama al JS extendido kmilaSetMonacoBreakpoints(all, untracked, warnMsg) → los breakpoints sin transición de señal se pintan como glifo gris hueco punteado + tooltip ⚠. StepReplayPanel también los marca en su lista. |
| Step-through en el esquemático (#3) | TrackActiveLines + LiveActiveLines/LiveActiveSignals + evento OnLiveStepChanged (throttle ~10 Hz, inmediato al pausar/step). SchematicViewer en modo Debug pulsa la compuerta cuyo Cell.SourceLine coincide con el delta actual (is-active-step) y enciende los wires de señales cambiadas; barra de control Pause/Resume/Step vía Replay.Live*. |
| Breakpoints en condiciones (#5) | El parser captura la línea del keyword en IfBranch.ConditionLine / CaseWhenBranch.ConditionLine; DeltaCycleEngine.OnConditionEvaluated(line) dispara cuando una rama if/elsif se toma o un arm case-when coincide; ReplayState.OnLiveConditionEvaluated pausa si hay un breakpoint en esa línea — el camino de transición-de-señal no puede atraparlas porque la condición no cambia ninguna señal. |
| Reattach a mitad de run (#11) | Editor.HydrateWaveformFromLiveEngine() reconstruye _waveformData desde Replay.LiveEngine.History al volver a un sim en vuelo. Abrir un run guardado durante el sim activo activa _liveMergeSuspended (los merges de progreso/fin no pisan la vista guardada); un banner "Volver a la corrida en vivo" rehidrata desde el engine. |
Esto cierra el HighlightLines vacío de la fila anterior: ahora el engine sí emite las líneas activas por delta. Ver memoria [kmila-backlog-2026-05-20] items #3/#4/#5/#11.
Detalle completo del polish pass 2026-05-17 (6 tareas, todas con sweep verde de 281+ tests): CHANGELOG_concepts_module_2026-05-17.md.
WaveformViewer — pipeline y arquitectura interactiva (2026-05-22)
La pestaña Waveforms del dock del editor renderiza, por cada señal de la simulación, una traza tipo step-function en SVG inline. El componente cubre tres caminos: visualización en vivo durante un run, snapshot estático tras completar, y modo compare superponiendo múltiples snapshots con paletas distintas. El detalle completo del rediseño 2026-05-22 (cierre de los items #8, #9, #10 del backlog) vive en 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
Decisiones de diseño clave (post-2026-05-22):
| Decisión | Razón |
|---|---|
| Inline SVG en lugar de Chart.js o canvas | Cada transición de la SignalHistory produce un paso real; el zoom solo redimensiona el ancho del SVG sin reinicializar pipeline de canvas. Migración heredada de cycles anteriores; documentada en el header del componente. |
| Pan-zoom DETACHED del waveform body | El cuerpo ya tiene overflow:auto. Mantener pan-zoom causaba dos mecanismos de pan simultáneos y, tras agregar el ocultado por señal (#9), las cotas cacheadas de clampPan quedaban obsoletas y el usuario podía arrastrar la traza fuera de pantalla. Schematic y Flow lo siguen usando porque son canvases libres. Ver [project_waveform_no_panzoom]. |
| Sticky chrome bidireccional | Columna izquierda con nombres (position: sticky; left: 0) + sombra de profundidad para hacer evidente el pinning; fila inferior del ruler (position: sticky; bottom: 0) para que el eje de tiempo siga visible al scrollear muchas señales. |
| Click-to-hide en la etiqueta de cada fila | Sin chrome extra; ícono de ojo aparece en hover. Una chip-bar arriba del body lista los ocultos para restaurar individualmente o en bulk. Aplica también a exports VCD/SVG/PNG. |
| Hover cursor JS en lugar de @onmousemove Razor | @onmousemove ruta por SignalR en Blazor Server; un cursor que actualiza 60 fps colapsa el circuito. JS lee data-view-* del body en cada frame, sin viajes a C#. |
| Reset = fit-to-width, no constante 1200 | kmilaWaveformFitWidth mide body.clientWidth - 162 y lo asigna a _zoomWidth (clamp 200..2.4M). En paneles anchos coincide con el default; en estrechos efectivamente cabe la traza completa. No auto-fit en mount: probado y revertido — comprime trazas densas a un bloque sólido ilegible. |
| Techo de zoom 2 400 000 px | Tras cuatro pasadas de afinamiento (24k → 96k → 200k → 600k → 2.4M) según feedback del usuario. Permite ~24 px por ciclo en el peor caso (100 MHz × 1 ms = 100k ciclos). Riesgo de stutter en iOS Safari al rendering de SVGs de tamaño extremo — el seguimiento correcto si surge como queja es virtualización de path (emitir solo el rango visible), no bajar el techo. |
Panel .kmila-dock__panel--waveforms con overflow:hidden |
Mismo idiom que el Schematic. Sin esta override, el panel del dock añadía su propio scrollbar vertical apilándose con el del body (el "tercer scrollbar" reportado por el usuario). |
| Filtrado en export | Los tres formatos (VCD, SVG, PNG) honran _hiddenSignals. Para SVG/PNG, se construye un WaveformData clonado superficialmente (los Transitions siguen compartidos por referencia — no hay copia de datos pesados). |
Componentes JS asociados (Kmila.Shared/wwwroot/js/app.js):
| Función | Propósito | Notas |
|---|---|---|
kmilaAttachWaveformHover(bodySelector) |
Inyecta <div class="waveform-hover__line"> + <div class="waveform-hover__chip"> en el body; listeners mousemove / mouseleave / scroll. |
Lee data-view-start-ns, data-view-end-ns, data-label-width del body en cada update(). Devuelve un id para detach. |
kmilaDetachWaveformHover(id) |
Remueve listeners + nodos DOM creados. | Llamado en Dispose() del componente. |
kmilaWaveformFitWidth(bodySelector, labelW) |
Devuelve body.clientWidth - labelW - 2, o -1 si no es medible. |
Usado por ZoomReset. |
kmilaSvgToPngBase64(svgString, scale) |
Rasteriza un SVG a base64 PNG vía <canvas> + drawImage. |
Compartido con el resto de exporters; sufrió el fix de tainted-canvas / font-import documentado en CHANGELOG_v1.19.md (item flow PNG). |
kmilaInlineSvgStyles(svgEl) |
Antes de exportar: inline-iza estilos computados y elimina @import, <foreignObject>, xlink:href externos. |
Mitiga el bug de tainted canvas. |
kmilaAttachPanZoom(rootSel, opts) |
NO usado por WaveformViewer. Sí por SchematicViewer + FlowDiagramViewer. | Documentación in-code del recipe contain: inline-size + min-width: 0. |
Limitaciones reconocidas y follow-ups:
- Performance a 2.4 M px — desktop OK, iOS Safari potencialmente lento. Fix correcto: emisión virtualizada del SVG path al rango visible (
viewStartNs..viewEndNs), no bajar el techo. - Quick-filter IN/OUT/INOUT del backlog #9 — diferido; el click-por-fila + chip-bar cubre el uso típico.
- Pinch-zoom en touch — sin equivalente al re-adjuntar pan-zoom. Si surge necesidad real (workflow tablet-first), agregar un handler pinch-only dedicado que actualice
_zoomWidth. - Time-range zoom (drag-select sobre la traza para zoom directo a un rango) — sería el siguiente paso natural y obviaría la necesidad de techos elevados.
Actualización 2026-09-13 — escalabilidad en corridas largas + lectura de valores
Motivación: en corridas de orden de segundos (p.ej. un display de 7 segmentos que actualiza cada ~50k ciclos) el visor congelaba la app entera. La causa no era el DOM (el dibujo ya estaba ventaneado), sino el costo algorítmico por render sobre el hilo único del circuito Blazor. Cambios (solo visualización; la lógica de simulación queda intacta y la salida VCD/CSV es byte-idéntica en corridas sin overflow):
- Acceso ventaneado + decimado (Fase 1/2).
WaveformSignal.GetWindow(startNs, endNs, maxPoints)(enModels/WaveformData.cs) hace búsqueda binaria (IndexAtOrBefore) del rango visible y devuelve a lo sumo ~1 punto por pixel, preservando primer/último y min/max de cada bucket para que pulsos rápidos (reloj) no desaparezcan.BuildTraceSvgconsume esa ventana. Se eliminó el hot-spot que copiaba la lista completa de transiciones (Transitions.ToArray()+Array.IndexOf) por señal-bus en cada render, y el escaneo lineal desde índice 0. Esto cierra el follow-up #1 (emisión virtualizada del path también en el escaneo de datos, no solo el dibujo). - Memoización de trazas.
TraceSvgCachedcachea el SVG por señal salvo cambio de ventana/zoom/ancho/color/#transiciones. Un movimiento de cursor NO reconstruye las trazas — solo los chips de valor —, evitando el render-storm. MergeFromO(1) por señal. ÍndiceDictionary<string, WaveformSignal>en vez delFirstOrDefaultpor nombre (era O(señales²) cada tick de 200 ms).- Columna de valor + toggle por señal. Cada fila muestra el valor de la señal en el
cursor (
WaveformData.ValueAt, binary search, last-write-wins — helper compartido conSchematicVieweren modo Debug) y un toggle onda↔valor; las señales numéricas (integer/ bus) arrancan en modo valor. Clases CSSwaveform-signal-value,waveform-row__mode-toggle,waveform-row__value-track,waveform-value-readout. - Cursor por hover.
kmilaAttachWaveformHover(body, dotNetRef)ahora publica el tiempo bajo el puntero (throttled ~60 ms) aWaveformViewer.OnCursorHover→SignalCursorBus. SetCursorTick(antes definido pero sin llamadores). El click en la regla sigue creando breakpoints de tiempo. Claves i18n:WAVEFORM_VALUE_AT_CURSOR,WAVEFORM_SHOW_AS_VALUE,WAVEFORM_SHOW_AS_WAVE. - Timestamp
longenSignalHistory(Fase 3a). El encodetick*1000+deltadesbordabaintpasado ~2.1 M ticks (~21 ms a 10 ns/tick), corrompiendo tiempos de onda/VCD en corridas largas. Se amplió alongsolo en la ruta de registro/almacenamiento; la ruta de atributos (SignalAttributeHandler.OnDeltaCommit,'stable(T)/'last_value) conserva suint— la semántica de simulación no cambia. 167/167 tests verdes. - Descartado: volcado a disco de
SignalHistory. Un spill transparente exigiría reescribir todas las rutas de lectura/export para ser segment-aware (riesgo a la garantía byte-idéntica de VCD/CSV y al replay) y choca con el watermark append-only deReadSince. El overflow ya está resuelto conlong; el único caso de RAM ilimitada (señal que cambia cada tick por segundos) ya tiene mitigaciones (KMILA_SIM_NO_HISTORY=1, ocultar reloj). No se implementó por no justificar el riesgo.
Actualización 2026-09-13 — buses expandibles por bit + etiqueta legible
Dos mejoras solo de visualización (la lógica de generación queda intacta y la salida
VCD/CSV es byte-idéntica; ver project_waveform_viz_only):
std_logic_vectorcolapsable → una onda por bit. El bus se sigue mostrando como una única fila agregada (lane GTKWave con etiqueta hex); un chevron (▸/▾) en su etiqueta lo expande en una sub-fila por bit (MSB arriba, etiquetanombre[n]en convencióndownto), cada una dibujada reutilizando la ramastd_logicdeBuildTraceSvgsobre señales sintetizadas ligeras (BitSignalsFor+BitTraceSvgCached, cacheadas pornombre#bit). Para conservar los estados U/Z/X por bit —que el valor numérico agregado colapsa— se añadió un campo display-onlyWaveformTransition.Bits(string crudo MSB-first) poblado solo para buses enFromSignalHistory(estático) yMergeFrom(vivo). Ese campo no lo lee ninguna ruta de export:Value/Labely el VCD no cambian. Estado de expansión en_expandedVectors(patrón espejo de_hiddenSignals). BitsU/Z/Xse renderizan como la traza discontinua "desconocido" ya existente, no como 0. Clases CSSwaveform-row__expand-toggle,waveform-row__expand-spacer,waveform-row--bit,waveform-row__bit-name. Claves i18nWAVEFORM_EXPAND_BITS,WAVEFORM_COLLAPSE_BITS.- Etiqueta nombre↔tipo sin colisión. El chip de dirección (
IN/OUT/INOUT/INTERNAL) iba a 4px del nombre mono, así que un nombre con "in"/"out"/"internal" se leía como parte del tipo. Se añadió un divisor (border-left+ padding) a la izquierda del nombre ytitlecon el nombre completo (el nombre se trunca con ellipsis). El modo compare ahora también envuelve chip+nombre+ancho en.waveform-row__label-mainpara heredar el mismo divisor y la contención de ellipsis (antes carecía de ambos). Solo CSS + markup.
Otros viewers (Schematic, Flow) y la utilidad kmilaAttachPanZoom
Para contexto comparativo:
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 | Estructura | Gestos | Razón |
|---|---|---|---|
SchematicViewer |
Free-form (gates + wires en posiciones X/Y arbitrarias) | Pan + pinch (kmilaAttachPanZoom) |
Sin un layout rectilíneo, scroll nativo no es suficiente: el usuario quiere centrar la vista, hacer zoom contextual, perseguir un wire largo. |
FlowDiagramViewer |
Free-form (Mermaid flowchart) | Pan + pinch (kmilaAttachPanZoom) |
Misma razón — el grafo emitido por Mermaid puede ser arbitrariamente ancho/alto y no se beneficia de scroll lineal. |
WaveformViewer |
Row-structured (filas alineadas por tiempo) | Scroll nativo + botones toolbar | Cada fila tiene la misma altura y comparte el eje X. Pan 2D libre no aporta sobre scroll; toolbar Zoom-In/Out/Reset cubre el cambio de relación de aspecto horizontal. |
Diagramas detallados por módulo (2026-05-22)
Esta sección expande el diagrama de alto nivel mostrando los servicios principales de cada módulo y el flujo de datos entre ellos a nivel de clase. Pensada para auditoría — los detalles más granulares (cada handler, helper, etc.) viven en los README.md de cada módulo.
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 corre la suite de 40 tests y reporta exit-code para 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: el Interpreter tiene una suite xUnit en Interpreter/Tests/ (correr con dotnet test), que al cierre del arco Tier 4.3 (2026-07-30) estaba en ~167 casos. El subconjunto de la pipeline IEEE/síntesis lo forman DeltaCycleEngineTests + SynthesisTests + IEEELibraryLoaderTests + LibraryCompilerTests + IEEEPrimitivesTests + IEEELoweringEndToEndTests + NestedIEEECallTests. Además existe el corpus de smoke test*.vhdl (54 fixtures) que corre Program.cs. Detalle y estado por superficie en ../Interpreter/README.md §7. (La cifra "82 cases / --test-delta" de revisiones previas era el subconjunto IEEE de v1.19.)
Modos de ejecución: Visual vs Fast + fix de timestamp >21 ms (2026-09-13)
Motivación: corridas de segundos de tiempo simulado (≥1 s a 10 ns/tick = 10⁸ ticks)
tardaban demasiado en el Editor. Se añadió un selector Fast/Visual en el Run-bar
(SimulationParameters.FastMode, toggle RunBar → leído por Editor.OnRunRequested):
Visual (por defecto): el
SimulationRunnerde referencia (bucletick++, visita cada tick) + forma de onda en vivo (timer de 200 ms enProgramBuilder) + replay por delta (ReplayState.AttachLive→ breakpoints/step/línea-activa). Sin cambios de comportamiento.Fast: dos aceleraciones combinadas, solo visualización/orquestación (la salida de simulación es idéntica):
- Menos efectos visuales:
ProgramBuilder.RunSimulationAsync(fastRun:true)omite el timer de 200 ms (no hay merge de historial ni redibujo de SVG por-frame) yEditoromiteReplay.AttachLive. Queda una barra de progreso barata (OnProgressChanged); la forma de onda completa se construye una vez al final (OnSimulationDataReady→FromSignalHistory). Pausa/Stop siguen funcionando (van por el runner, no por ReplayState); breakpoints/step no. - Motor "duplicado" dirigido por eventos: nuevo
Interpreter/Services/FastSimulationRunner.cs— hermano delSimulationRunnerque conduce el mismoDeltaCycleEngine, pero salta los ticks ociosos: avanza amin(próximo flanco de reloj, próximo estímulo,SignalScheduler.NextScheduledTick,DeltaCycleEngine.NextProcessWakeTick, sello final). Es correcto porque ambas colas del motor drenan todo lo vencido en<= currentTick(SignalScheduler.AdvanceTime,DrainReadyProcessWakesAsync), y un tick ocioso no produce delta → ni cambio de historial ni de señal. El motor ySimulationRunnerquedan intactos; la única adición al motor es el getter de solo-lecturaDeltaCycleEngine.NextProcessWakeTick(espejo deNextScheduledTick, sin comportamiento). Ambos runners implementanISimulationRunnerpara queProgramBuilderelija uno en runtime.
Equivalencia byte-idéntica verificada por
FastRunnerEquivalenceTests(VCD de Fast == VCD de referencia para reloj, flip-flop con flanco, ywait for). Sonda de velocidad: 50 000 ticks con reloj lento (1 MHz @ 10 ns) → referencia ~97 ms vs Fast ~7 ms (~14×), salida idéntica. Nota: si el reloj conmuta cada tick (p.ej. 50 MHz @ 10 ns), no hay ticks ociosos que saltar y la ganancia de Fast es solo la de "menos efectos visuales"; para bajar el piso de 10⁸ ticks ahí, la palanca es una resolución de tick más gruesa (cambia granularidad, aparte). Pendiente menor: persistir el modo entre sesiones (hoy es estado de sesión).- Menos efectos visuales:
Fix de timestamp
int→long(>21 ms), correctness — autorizado por el usuario. El sello de tiempo del camino de atributos seguía enint((int)(_currentTick*1000)), que desbordaInt32.MaxValuepasadotick ≈ 2.147×10⁶(~21.5 ms a 10 ns/tick), corrompiendo'stable/'last_value/'event/'delayedy los timestamps de transición de clock/estímulo. Se ensanchó along:SignalAttributeHandler._lastChangeMoments/_currentTimeMoment/OnDeltaCommit/StampStimulusChanges/GetStable, eltimeMomentenDeltaCycleEngine.RunDeltaCycleAsync, y los dos casts(int)(tick*1000)enSimulationRunner. Cambia la salida solo para corridas >21 ms (de incorrecta a correcta); <21 ms queda idéntica. Suite: 170/170 verde (167 previos + 3 de equivalencia).ETA de ejecución + log de rendimiento (2026-09-13).
SimulationParametersestima el tiempo restante en vivo (RemainingSeconds/ElapsedSeconds) desde el ritmo observado de progreso (remaining = elapsedActivo × (100−%)/%, excluyendo el tiempo en pausa; se abstiene hasta ≥1 % y ≥0.3 s para no mostrar números disparatados). El Run-bar lo muestra bajo la barra de progreso ("~5s restante" / "estimando…" / "en pausa"). No hay predicción previa fiable (el costo por tick varía por diseño), así que se calibra sobre la marcha.ProgramBuilderemite además una línea[Sim.Perf]a /logs (mode,elapsed,totalTicks, y en FastvisitedTicks- % de ticks ejecutados) para diagnosticar por qué una corrida fue lenta. Tests en
SimulationEtaTests; suite 172/172.
- % de ticks ejecutados) para diagnosticar por qué una corrida fue lenta. Tests en
Interpreter — synthesis path (modo "FPGA visualization")
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 gana en v1.19 (#29) el case DeferredFunction nested que recursivamente baja llamadas IEEE anidadas (to_integer(unsigned(addr))) a un grafo de cells correctamente cableado.
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 es el contrato que hace que la pestaña Output del dock siempre tenga contenido útil, aún cuando un Parser exception tumbó el árbol estructural.
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
Detalles: IEEE_PIPELINE_GUIDE.md y 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
Notas operativas:
- El 64 MB cap del SignalR de Blazor Server (vide [
project_blazor_signalr_message_size]) limita el tamaño de los bytes que pueden viajar JSInterop → C# en un solo round-trip. Diagramas extremadamente grandes (10k+ nodos de Mermaid Flow) podrían requerir un trigger directo en JS (Blob+anchor) en vez del round-trip actual. kmilaInlineSvgStyleses el cierre del bug de tainted-canvas reportado en v1.19 (CHANGELOG entry "Flow PNG / SVG export broken"). Sanitiza@import,<foreignObject>,<image>externo,xlink:hrefantes deldrawImageque rasteriza.
UI surface — pestañas del Editor dock
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
Cada pestaña es un RenderFragment named param de EditorDock. La pestaña activa se persiste por proyecto en EditorDockState + localStorage. La pestaña Replay apareció en el polish pass 2026-05-17; Flow apareció con el export UML 2026-05-21 (vide [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: los bloques de Fase 0–2 documentados abajo son la base; el runtime hoy tiene el conjunto completo de 12 widgets + bit-slice bindings + bounce + 4 example gallery cases (Fase 3), plus canvas record + export + locator + pan-wall (Fase 3+). Ver
VISUAL_MODE.md §10-12para el estado actual de los widgets yCHANGELOG.md"Visual Runtime — full set + slices …" para el detalle de shipping.
Modo de simulación complementario al bounded run: vincula los puertos del
diseño a componentes virtuales (LED, botón) en un canvas y deja al estudiante
interactuar. Reutiliza al 100% el motor de simulación existente — no hay un
segundo 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
Piezas en naranja = preexistentes en el simulator core (no se tocan).
Piezas en azul = nuevas en Kmila.Shared/Services/VisualRun/ y
Kmila.Shared/Components/VisualRun/.
El seam crítico es el ciclo coordinator.LoopAsync → escribe puertos →
engine.AdvanceTime(tick) → engine.RunTimeStepAsync() → snapshot.
Para detalles del contrato de cada componente, los puertos soportados, el
schema .kviz y el roadmap fase 1+, ver VISUAL_MODE.md. El plan
original y las decisiones bloqueadas viven en
HANDOFF_visual_runtime_2026-06-24.md.
Fase 1A/B (2026-06-25): dos componentes nuevos (SwitchWidget,
LedArrayWidget) y un auto-clock dentro del coordinador. Cualquier
puerto IN cuyo nombre lea como reloj (clk, clock, aclk, …) se
toggea cada CLOCK_HALF_PERIOD_TICKS = 8 ticks — salvo que el usuario
haya cableado un Switch/Button a ese puerto. Visual.razor recolecta los
puertos manejados por el usuario y los pasa como userDrivenPorts al
constructor del coordinador, que los excluye de su lista interna
_clockPorts. Esto permite que la counter4 fixture cuente sin estímulo
externo, mientras que un diseño con clk manual sigue funcionando.
Fase 2 (2026-06-25): se cierran los cinco entregables de HANDOFF §8
en una sola sesión:
- Speed slider + custom ms/tick — el coordinador acepta un override
CustomTickDelayMs: int?; cuando esnullcae al presetSpeed.TickDelayMs(). La página mapea un<input type="range">0..3 al enumVisualSpeedy un<input type="number">al override. - Pause / Continue —
coordinator.Pause()/Resume()con un loop que duerme 20 ms entre chequeos mientrasIsPaused. - Tick breakpoints —
TickBreakpoints: HashSet<ulong>consultado al inicio de cada tick; al match el loop entra en pausa, disparaOnBreakpointHit(tick)y se arma el sentinel_lastBreakTickpara queResume()no vuelva a romper en el mismo tick. - Frame scrubbing —
FrameRing.Nearest(tick)busca el frame más cercano dentro de la ventana del anillo. Mientras está en pausa,Pages/Visual.razor.ApplyScrubFrame()reescribeport.Valuepara cada puerto del entity desdeframe.PortValues; los widgets re-leen y redibujan.Resume()deja que el siguiente tick sobrescriba todo. StepperWidget— cuarto widget interactivo. Decodifica 4 pinesphaseA/B/Ān/B̄ncontra la tabla canónica de 8 patrones Gray; calcula el deltaWrappedDelta(prevIdx, newIdx)para acumular pasos con wrap-around correcto. El rotor (transform: rotate(...)) gira360 / stepsPerRevpor paso.
A nivel de persistencia se agrega el setting KMILA_VISUAL_RING_BUFFER
(256..16384, default 2000) en AppSettings. Visual.razor lo lee en
OnInitialized y toma Math.Max(perDoc, settings) para dimensionar el
FrameRing del coordinador.
7-seg — modos de decodificación + polaridad (2026-09-14): el banco de
7 segmentos (SevenSegmentBankWidget) gana el mismo mode que el dígito
suelto — raw (bus empacado de segmentos, ancho digits×7), hex y dec
(bus empacado de nibbles, ancho digits×4, decodificado internamente). Ambos
widgets ganan además una config polarity: cathode (default, activo-alto,
comportamiento previo) o anode (activo-bajo, invierte el bit leído). La
polaridad aplica solo en modo raw; en modos decodificados el widget elige
qué segmentos encienden. La tabla de patrones y el decode se extraen a
Services/VisualRun/SevenSegPatterns.cs (reuso entre ambos widgets). Es un
cambio solo de visualización: no toca el motor ni la salida de simulación,
y los .kviz existentes se renderizan igual (defaults raw/cathode). Sin
cambios de schema (Config es un diccionario abierto) ni de pines
(KvizWidgetCatalog). Contratos por widget en VISUAL_MODE.md §5.
Buzzer — corte de audio al detener (2026-09-14): el BuzzerWidget emitía
el tono desde OnAfterRenderAsync según los valores de puerto, que se
congelan al detener o pausar la simulación (incluida la auto-pausa por cambio
de pestaña) — el oscilador de Web Audio seguía sonando tras parar. Corregido
atando el estado audible al ciclo de ejecución: IsActive ahora exige
Coordinator { IsRunning: true, IsPaused: false } (CoordinatorActive), de modo
que el siguiente render tras StopRun/Pause empuja silencio. Además
VisualPanel llama a kmilaAudio.stopAll() (nuevo en kmila-audio.js) al
detener y al ocultar la pestaña, como corte inmediato. Solo visualización/audio;
sin cambios en el motor.
7-seg bank — modo multiplexado (2026-09-14): el banco gana un cuarto
mode = "mux" que modela un display multiplexado real: todos los dígitos
comparten las líneas de segmento (a..g) y cada dígito tiene su propia línea
común / de selección (ánodo/cátodo) que el diseño estrobea de a uno. El
wiring (config, solo mux) ofrece ambos estilos de cableado pedidos por el
usuario: packed (dos vectores seg + sel, orden asumido, compacto) o
explicit (pines individuales a..g + an0..an{digits-1}, cada uno cableable
por separado). selectActiveLow (default true) fija si el dígito se habilita
con 0 (convención an de las tarjetas) o con 1. Como a velocidad de
simulación solo un dígito está seleccionado por tick, con persistence (default
true) el widget retiene por dígito el último patrón visto mientras su
selección estuvo activa, recorriendo hacia atrás el FrameRing
(VisualRunCoordinator.Frames) — sin estado nuevo en el motor ni cambios en
DeltaCycleEngine. polarity ahora aplica también a los segmentos en mux.
KvizWidgetCatalog.PinsOf se vuelve dependiente de modo/wiring para el banco,
y OnSevenSegBankModeChange/DigitsChange/WiringChange llaman a Sanitize para
podar bindings obsoletos al cambiar el conjunto de pines. Sigue siendo solo
visualización; los .kviz previos se renderizan igual (default raw).
Última actualización: 2 de mayo de 2026 (datos de módulos); 9 de mayo de 2026 (auditoría de diagramas); 17 de mayo de 2026 (polish pass + step-replay); 22 de mayo de 2026 (overhaul de WaveformViewer + diagramas detallados por módulo); 24 de junio de 2026 (Visual Runtime Phase 0 seam); 25 de junio de 2026 (Phase 1A/B Switch+LED array + Phase 2 speed/scrub/bp/stepper); 13 de septiembre de 2026 (WaveformViewer: ventaneo+decimación por zoom, columna de valor + toggle por señal, cursor por hover, timestamp long en SignalHistory); 14 de septiembre de 2026 (7-seg bank: modos raw/hex/dec + polaridad cátodo/ánodo en ambos widgets de 7 segmentos; modo multiplexado mux con cableado packed/explicit, nivel de selección y persistencia de visión; fix del buzzer que seguía sonando tras detener/ocultar la pestaña).