feat: integra renaser (kernel SASOS bare-metal) al monorepo
renaser —kernel asíncrono de espacio de direcciones único, no-POSIX,
`no_std` x86_64— entra al monorepo como su PROPIO workspace de Cargo,
no fusionado: usa toolchain nightly, target `x86_64-unknown-none` y
`panic = "abort"`, incompatibles con los perfiles globales de brahman.
- `renaser/` — copia del proyecto (sin su `.git`; el repo original
conserva su historia standalone). Workspace propio con su
`rust-toolchain.toml` y `.cargo/`.
- `exclude = ["renaser"]` en el workspace de brahman: Cargo lo trata
como ajeno.
- El kernel de renaser path-depende `mirada-layout` cruzando la
frontera de workspace — primer núcleo compartido. Semilla de la
Fase 8 (compositor): geometría de teselado compartida, framebuffer
nativo de renaser; smithay se queda en el lado Linux.
Verificado: `cargo build -p boot` compila kernel + imagen UEFI con
mirada-layout enlazado para bare-metal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
diff --git a/Cargo.toml b/Cargo.toml
index f4ed048..5209af0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -302,6 +302,13 @@ members = [
"crates/apps/charka",
]
+# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
+# PROPIO workspace de Cargo: usa toolchain nightly, target
+# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
+# perfiles globales de este workspace. Cargo lo trata como ajeno; los
+# crates compartidos se referencian por `path` cruzando la frontera.
+exclude = ["renaser"]
+
[workspace.package]
version = "0.1.0"
edition = "2021"
diff --git a/renaser/.cargo/config.toml b/renaser/.cargo/config.toml
new file mode 100644
index 0000000..e7ab80c
--- /dev/null
+++ b/renaser/.cargo/config.toml
@@ -0,0 +1,19 @@
+# =============================================================================
+# renaser :: configuracion de cargo
+# -----------------------------------------------------------------------------
+# El kernel se compila contra el target NATIVO precompilado `x86_64-unknown-none`
+# (bare-metal, soft-float, sin SSE). No hay JSON propio ni `build-std`: cero
+# burocracia del compilador, aislamiento de arquitectura sin friccion.
+# =============================================================================
+
+[unstable]
+# Dependencias de artefacto (RFC 3028): permiten que el miembro `boot` ordene a
+# cargo compilar el kernel para `x86_64-unknown-none` de forma automatica y
+# reciba la ruta de su ELF. Sigue siendo una funcion inestable (nightly).
+bindeps = true
+
+[alias]
+# Compila unicamente el kernel, en aislamiento de arquitectura.
+kernel = "build -p kernel --target x86_64-unknown-none"
+# Construye la imagen UEFI y abre QEMU (equivale a `cargo run -p boot`).
+qemu = "run -p boot"
diff --git a/renaser/.gitignore b/renaser/.gitignore
new file mode 100644
index 0000000..39e8e2a
--- /dev/null
+++ b/renaser/.gitignore
@@ -0,0 +1,24 @@
+# =============================================================================
+# renaser :: archivos que NO se versionan
+# =============================================================================
+
+# Artefactos de compilacion de Rust — kernel, boot y apps WASM.
+/target
+/kernel/target
+/apps/*/target
+
+# Bloqueo de dependencias del kernel: se genera al compilarlo en aislamiento.
+# La verdad de versiones la fija el Cargo.lock raiz — el kernel es dependencia
+# de artefacto de `boot` y se resuelve con el.
+/kernel/Cargo.lock
+
+# Ajustes locales del entorno de desarrollo (personales, no compartidos).
+/.claude/settings.local.json
+
+# Capturas temporales de QEMU.
+*.ppm
+*.png.tmp
+
+# Borrador de trabajo local — puede contener notas y credenciales; nunca se
+# versiona. (Fue subido por error y purgado del historial; ver el README.)
+/renaser.txt
diff --git a/renaser/ARCHITECTURE.md b/renaser/ARCHITECTURE.md
new file mode 100644
index 0000000..acf60fa
--- /dev/null
+++ b/renaser/ARCHITECTURE.md
@@ -0,0 +1,173 @@
+# renaser — Arquitectura
+
+Este documento describe la arquitectura del kernel `renaser` subsistema a
+subsistema. Para el estado por fases, ver `ROADMAP.md`.
+
+## 1. Filosofía
+
+renaser es un **SASOS** — *Single Address Space Operating System*. No hay un
+espacio de direcciones por proceso ni cambios de contexto de hardware: todo el
+sistema —kernel y aplicaciones— comparte una única RAM plana.
+
+El aislamiento NO lo da la MMU ni los anillos de privilegio de la CPU. Lo da el
+**software**: las aplicaciones se distribuyen como bytecode WebAssembly y se
+ejecutan dentro de un intérprete que acota matemáticamente cada acceso a
+memoria. Una aplicación solo puede hacer aquello para lo que el kernel le haya
+inyectado una *capacidad* (una función de host). Lo que no está importado no
+tiene camino físico que recorrer.
+
+La interfaz es **visual desde el primer microsegundo**. No hay TTY; el texto se
+rasteriza como gráfico vectorial. "El texto es un caso particular del dibujo."
+
+## 2. La cadena de arranque
+
+```
+Firmware UEFI → crate `bootloader` 0.11 → kernel::_start → kernel_main
+```
+
+El miembro `boot/` (que corre en el **anfitrión**) toma el ELF del kernel,
+lo fusiona con el cargador `bootloader` 0.11 en una imagen de disco UEFI GPT y
+la lanza en QEMU con el firmware OVMF. El cargador deja la CPU en modo largo de
+64 bits, mapea el kernel y le entrega una `BootInfo` con el framebuffer GOP.
+
+## 3. Estructura del espacio de trabajo
+
+El workspace tiene **un solo miembro**, `boot`. El `kernel` y las `apps` están
+**excluidos** a propósito:
+
+- El kernel es `#![no_std] #![no_main]` y define su propio `_start`. Si fuese
+ miembro del workspace, `cargo build` intentaría compilarlo para el anfitrión
+ y fallaría por un símbolo `_start` duplicado con el `crt0` del sistema.
+- Por eso el kernel se construye **solo** como *dependencia de artefacto*
+ (RFC 3028) de `boot`, que le fija el target `x86_64-unknown-none`.
+- Las apps WASM tienen su propio target (`wasm32-unknown-unknown`) y se
+ compilan aparte.
+
+El kernel usa el target **nativo precompilado** `x86_64-unknown-none`: sin
+target JSON propio, sin `build-std`. Soft-float y sin SSE, para que las
+interrupciones no corrompan registros de punto flotante.
+
+## 4. Subsistemas del kernel
+
+### `grafico` — el sustrato visual
+`Color` (RGB de 24 bits, independiente del hardware), `Pantalla` (el
+framebuffer GOP físico, envuelto en seguridad) y `Lienzo` (el búfer intermedio
+en RAM). Toda composición ocurre en el `Lienzo`; `Pantalla::presentar` lo
+vuelca de un solo gesto con escrituras volátiles — **doble búfer**, sin
+desgarros. `codificar` traduce un `Color` al formato nativo del framebuffer.
+
+### `consola` — la superficie de texto e imagen
+Une `Lienzo` + `Pantalla` + una pluma de escritura. Rasteriza glifos con
+`fontdue` y los funde sobre el lienzo mezclando por cobertura (anti-aliasing).
+También vuelca fotogramas crudos del userspace WASM. Es global, tras un `Mutex`.
+
+### `baliza` — la red de seguridad visual
+Publica de forma atómica y sin cerrojos los datos mínimos del framebuffer. Los
+manejadores `#[panic_handler]` y `#[alloc_error_handler]` los usan para pintar
+una franja de advertencia —**roja** si el sistema colapsa, **naranja** si el
+heap se agota— escribiendo directo sobre el silicio, sin confiar en el heap ni
+en estructura dinámica alguna.
+
+### `gdt` — cimientos del manejo de fallos
+GDT propia con segmentos de código y datos del kernel, y un TSS cuyo único
+cometido es alojar un **stack de emergencia** (IST) para el doble fallo: ni un
+desbordamiento de la pila del kernel impide su diagnóstico.
+
+### `interrupts` — la tabla de reflejos
+IDT con manejadores de excepción de CPU (breakpoint recuperable; el resto,
+fatales → `panic!`) e interrupciones de hardware. El doble fallo se atiende
+sobre el stack de emergencia del TSS.
+
+### `pic` — el latido del hardware
+Remapea el par 8259 (PIC) fuera del rango de las excepciones de CPU y programa
+el temporizador de intervalos (PIT). El teclado (IRQ1) deposita scancodes en
+una cola lock-free. `desenmascarar` abre una línea de IRQ concreta —la del
+disco, descubierta en tiempo de ejecución (Fase 6.2)—.
+
+### `drivers` — el hardware que el kernel conquista (Fases 6.1, 6.2)
+A diferencia del framebuffer o el temporizador —que el firmware sirve en
+bandeja—, el disco hay que DESCUBRIRLO y reclamarlo.
+- `pci` — `CamPuertos`: acceso al espacio de configuración del bus PCI por el
+ mecanismo #1 (puertos `0xCF8`/`0xCFC`); `linea_irq` lee la IRQ del dispositivo.
+- `disco` — el disco virtio-blk. Un asignador de marcos por **mapa de bits**
+ —con liberación real— reparte páginas físicas para el DMA; `KernelHal`
+ implementa el `trait Hal` de `virtio-drivers` (traducción de direcciones,
+ memoria rebote para el DMA); el `VirtIOBlk` se monta una vez y persiste tras
+ un `Mutex`. La E/S de bloques es **asíncrona** (Fase 6.2): `EsperaDisco` es un
+ `Future` que envía la petición por la API no bloqueante de `virtio-drivers`,
+ cede la CPU y se reanuda con la IRQ del disco (`atender_irq`). `bloquear_en`
+ conduce ese `Future` desde los contextos síncronos durmiendo la CPU con
+ `hlt`; sobre él, `leer_sectores`/`escribir_sectores` sirven al grafo de
+ objetos. La IRQ del disco se enruta por el PIC 8259, no por el IOAPIC.
+
+### `almacen` — el grafo de objetos direccionado por contenido (Fase 6.1c)
+renaser no tiene un sistema de archivos plano POSIX: tiene un DAG de objetos.
+Un `Objeto` es una carga útil de bytes y una lista de aristas (hashes de
+hijos). La identidad de un objeto es el hash BLAKE3 de su forma serializada
+(`postcard`) — de ahí integridad (un objeto se verifica al leerlo) y
+deduplicación (contenido idéntico, un solo registro). El disco es un **log**:
+el sector 0 es el superbloque (magia, versión, cursor, raíz); tras él se anexan
+los registros de objetos. Un índice hash→sector se reconstruye al arrancar
+recorriendo el log. Las capacidades `sys_object_*` exponen el grafo al
+userspace.
+
+### `memory` — el heap dinámico
+`linked_list_allocator` como `#[global_allocator]` sobre una región estática de
+64 MiB en `.bss`. Desbloquea `alloc::*` — `Box`, `Vec`, `BTreeMap`, `Arc`.
+
+### `async_system` — el reactor cooperativo
+- `task` — `Task`: un `Future` anclado (`Pin<Box<dyn Future>>`) con `TaskId`.
+- `executor` — el `Executor`: censo de tareas, cola de listas, y un `hlt`
+ controlado cuando no hay trabajo.
+- `waker` — un `Waker` que reinyecta el `TaskId` en la cola al despertar.
+- `teclado` — los canales de scancodes. Cada app abre el suyo; la IRQ1 **difunde**
+ cada scancode a todos, de modo que varias apps reciben la entrada en paralelo.
+- `reloj` — convierte la IRQ0 (PIT, 100 Hz) en el `Future` `EsperaFrame`, que se
+ resuelve en el siguiente pulso. Es el **compás de los fotogramas**: una tarea
+ WASM hace su trabajo de un fotograma y `.await`-ea el siguiente.
+
+Las interrupciones de hardware no conmutan el contexto de la CPU: **despiertan
+tareas**. El kernel avanza cooperativamente.
+
+### `texto` — tipografía vectorial
+Empotra un `.ttf` en el binario (`include_bytes!`) y lo rasteriza con `fontdue`
+glifo a glifo, bajo demanda.
+
+### `wasm` — el escudo de aislamiento
+- `mod` — el runtime y la `AplicacionWasm`: una instancia **persistente** entre
+ fotogramas (`Store` + `TypedFunc<(), ()>` + región). El ABI del userspace es
+ `init()` (una vez) y `tick()` (un fotograma, y retorna). Dos guardarraíles
+ acotan a cada app: el **temporal** —un presupuesto de **combustible** (`fuel`)
+ por `tick`; agotarlo la desaloja (baliza púrpura)— y el **espacial** —un techo
+ de memoria lineal vía `Store::limiter`; rebasarlo la desaloja (baliza
+ amarilla)—. En ambos casos `wasmi` lanza una trampa, el kernel recupera el
+ mando y el sistema no sufre. El `Drop` de `AplicacionWasm` reconcilia el ciclo
+ de vida: da de baja su canal de teclado.
+- `env` — la **matriz de capacidades**: las funciones de host que el módulo
+ WASM puede invocar. Hoy son siete:
+ - `sys_render_frame(ptr, len)` — compone un fotograma dentro de la **región**
+ de pantalla asignada a la app. El kernel valida **matemáticamente** que
+ `[ptr, ptr+len)` cae dentro de la memoria lineal del módulo, y que el
+ tamaño es el de la región, antes de leer un byte; si no, aborta la app.
+ - `sys_get_scancode()` — entrega, sin bloquear, el siguiente scancode del
+ canal de teclado **propio** de la app.
+ - `sys_object_put` / `sys_object_datos` / `sys_object_hijo` /
+ `sys_object_raiz` / `sys_object_fijar_raiz` — el acceso al grafo de objetos
+ persistente (ver `almacen`): grabar un objeto, leer su carga útil, recorrer
+ sus aristas, y leer o fijar la raíz del DAG. Cada puntero que la app
+ entrega se valida contra su memoria lineal, igual que en `sys_render_frame`.
+
+Todo puntero inválido aborta la app —es su culpa: el `Error` se traduce en una
+trampa de WASM, la app se desaloja y el resto del sistema sigue—. Un fallo del
+almacenamiento, en cambio, no es culpa de la app: se le devuelve un código de
+error que ella decide cómo afrontar.
+
+## 5. Restricciones de ingeniería
+
+- **Escrituras volátiles** obligatorias para framebuffer y MMIO.
+- **`unsafe`** confinado en células mínimas, cada una con un contrato
+ `SEGURIDAD:` y envuelta en una abstracción segura. `unsafe_op_in_unsafe_fn`
+ está en `deny`.
+- **Alineación**: 16 bytes para estructuras genéricas; 4096 (página) para
+ buffers de asignador y el lienzo de respaldo.
+- **`no_std` estricto**: `core` + `alloc`; nada de `std`.
diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
new file mode 100644
index 0000000..db1b321
--- /dev/null
+++ b/renaser/CHANGELOG.md
@@ -0,0 +1,457 @@
+# Registro de cambios — renaser
+
+Registro técnico detallado, fase a fase, en orden cronológico. Formato
+inspirado en *Keep a Changelog*. Para la crónica en lenguaje llano, ver
+`DIARIO.md`; para el estado y los planes, `ROADMAP.md`.
+
+---
+
+## Fase 1 — El primer microsegundo — 2026-05-21
+
+### Infraestructura
+- Espacio de trabajo Cargo, `kernel/Cargo.toml`, `.cargo/config.toml`,
+ `rust-toolchain.toml`.
+- Target inicial: especificación JSON propia `x86_64-renaser.json` — PIC,
+ soft-float, sin SSE, enlazador `lld`.
+
+### Añadido
+- `kernel/src/main.rs`: punto de entrada con el macro `entry_point!` de
+ `bootloader_api` 0.11.
+- Adopción y verificación con seguridad de tipos del framebuffer GOP (ancho,
+ alto, formato de píxel, *stride*).
+- Lienzo intermedio estático de 8 MiB en `.bss`, alineado a página: la técnica
+ de doble búfer para evitar parpadeos.
+- `#[panic_handler]` que dibuja una franja roja directamente sobre el
+ framebuffer mediante escrituras volátiles.
+- Dependencias: `bootloader_api` 0.11, `x86_64` 0.15, `embedded-graphics` 0.8.
+
+### Notas
+- Corrección clave: `bootloader` (constructor de imagen, lado anfitrión) no es
+ `bootloader_api` (la API `no_std` que consume el kernel).
+
+---
+
+## Fase 1.5 — Empaquetado y arranque — 2026-05-21
+
+### Cambiado
+- Migración del target del kernel: de la JSON propia `x86_64-renaser.json` al
+ target nativo precompilado `x86_64-unknown-none`. Elimina `build-std` y la
+ burocracia de compilador asociada.
+
+### Añadido
+- Miembro `boot/`: orquestador de anfitrión. Dependencia de artefacto
+ (RFC 3028) sobre el kernel; construcción de la imagen de disco UEFI con
+ `bootloader::UefiBoot`; lanzador de QEMU.
+
+### Toolchain
+- Instalación de la toolchain nightly (la máquina solo tenía la estable).
+
+### Corregido
+- Los nightly recientes exigen `-Zjson-target-spec` para usar specs JSON
+ (resuelto al migrar al target nativo).
+- Sintaxis del acelerador de QEMU: `-machine q35,accel=kvm:tcg`.
+
+### Verificado
+- Arranque en QEMU: superficie índigo limpia a 1280×800.
+- Baliza de pánico (franja roja) confirmada inyectando un pánico de prueba.
+
+---
+
+## Fase 2.0 — Cimientos del manejo de fallos — 2026-05-21
+
+### Añadido
+- `kernel/src/gdt.rs`: GDT propia, TSS y un stack de emergencia (IST)
+ reservado para el manejador de doble fallo.
+- `kernel/src/interrupts.rs`: IDT con manejadores de excepción de CPU. El
+ breakpoint (#BP) es recuperable; #UD, #DE, #GP, #PF y #DF son fatales y
+ encienden la baliza.
+- `#![feature(abi_x86_interrupt)]`.
+
+### Corregido
+- La GDT debe recargar los registros SS/DS/ES con un segmento de datos del
+ kernel: el cargador deja `SS = 0x10`, valor que en la GDT nueva pasa a ser el
+ descriptor del TSS; el primer `iretq` de una rutina de excepción provocaba un
+ #GP. Diagnosticado con la traza `-d int` de QEMU.
+
+### Verificado
+- Excepción `int3` atrapada y superada; línea-latido teal dibujada como prueba.
+
+---
+
+## Fase 2.1 — Interrupciones de hardware — 2026-05-21
+
+### Añadido
+- `kernel/src/pic.rs`: remapeo del par 8259 (PIC) fuera del rango de las
+ excepciones (vectores 0x20+); programación del temporizador PIT a 100 Hz;
+ IRQ1 de teclado.
+- Bucle de render en `kernel_main`, despertado por el temporizador, en
+ sustitución del `hlt` estático.
+
+### Verificado
+- La línea-latido se anima (temporizador en marcha); las pulsaciones de teclado
+ inyectadas cambian el cuadro-eco (teclado en marcha).
+
+---
+
+## Fase 3 — Memoria dinámica y reactor asíncrono — 2026-05-21
+
+### Añadido
+- `kernel/src/memory/`: `linked_list_allocator` como `#[global_allocator]`
+ sobre una región estática en `.bss`.
+- `#[alloc_error_handler]`: franja naranja de agotamiento de memoria.
+ `#![feature(alloc_error_handler)]`.
+- `kernel/src/async_system/`: reactor cooperativo — `Executor`, `Task`/`TaskId`,
+ `Waker` (vía `alloc::task::Wake`) y `ScancodeStream`.
+- `kernel/src/texto.rs`: rasterización de tipografía vectorial con `fontdue`;
+ TTF Adwaita Mono empotrada con `include_bytes!`.
+- Dependencias: `linked_list_allocator` 0.10, `spin` 0.9, `crossbeam-queue`
+ 0.3, `futures-util` 0.3, `fontdue` 0.9.
+
+### Cambiado
+- Heap ampliado de 16 MiB a 64 MiB: `fontdue`, al analizar una tipografía real,
+ agotaba los 16 MiB iniciales. El `#[alloc_error_handler]` lo delató pintando
+ la franja naranja — el guardarraíl funcionó en silicio real.
+
+### Corregido
+- El modo `no_std` de `fontdue` está condicionado a su feature `hashbrown`; sin
+ ella recae en `std`. Se fija `default-features = false, features = ["hashbrown"]`.
+
+### Verificado
+- Rótulo de bienvenida rasterizado al arranque; el texto tecleado aparece en vivo.
+
+---
+
+## Fase 4 — El escudo de aislamiento WASM — 2026-05-21
+
+### Añadido
+- `kernel/src/wasm/`: intérprete `wasmi` 1.0.9 en modo `no_std`. `env.rs`
+ define la matriz de capacidades — exactamente dos funciones de host:
+ `sys_render_frame(ptr, len)`, con validación infranqueable de los límites de
+ la memoria lineal, y `sys_get_scancode()`.
+- `apps/hello_wasm/`: primera aplicación del userspace — módulo
+ `wasm32-unknown-unknown` (cdylib), un cuadrado móvil dirigido por teclado. Se
+ empotra en el kernel con `include_bytes!`.
+- Dependencia: `wasmi` 1.0.
+
+### Cambiado
+- El kernel se EXCLUYE del espacio de trabajo (`exclude = ["kernel", "apps"]`):
+ al añadir `wasmi`, `cargo build` intentaba compilar el kernel para el
+ anfitrión y fallaba por un símbolo `_start` duplicado. El kernel pasa a
+ construirse únicamente como dependencia de artefacto de `boot`.
+
+### Corregido
+- `wasmi` `no_std` necesita `default-features = false, features = ["hash-collections"]`.
+- `wasmi` 1.0.9 usa `Linker::instantiate_and_start` (no existe `instantiate`).
+
+### Verificado
+- La aplicación WASM pinta su propia superficie y responde a las pulsaciones de
+ teclado inyectadas, sin más vía hacia el kernel que las dos capacidades.
+
+---
+
+## Mantenimiento — Estructura, documentación e integración — 2026-05-21
+
+### Cambiado
+- Refactorización: `main.rs` dividido de 692 a ~155 líneas. Se extraen los
+ módulos `sync.rs`, `grafico.rs`, `consola.rs` y `baliza.rs`. Sin cambios de
+ comportamiento (verificado en QEMU).
+
+### Añadido
+- Documentación: `CLAUDE.md`, `README.md`, `ARCHITECTURE.md`, `ROADMAP.md`,
+ `CHANGELOG.md` y `DIARIO.md`.
+- Integración con git: repositorio inicializado, remoto `origin` en Gitea,
+ `.gitignore`.
+
+### Seguridad
+- `renaser.txt` —un borrador de trabajo que contenía una credencial— se subió
+ por error en el commit inicial. Se purgó de todo el historial con
+ `git filter-branch` y se reescribió el remoto con `push --force`. La
+ credencial expuesta debe rotarse.
+
+---
+
+## Fase 5 — Multitarea cooperativa, guardarrail de fuel y reloj — 2026-05-22
+
+Unificación de la Fase 3 (reactor) y la Fase 4 (WASM): el userspace deja de ser
+una sola app que monopoliza la CPU y pasa a ser un conjunto de aplicaciones
+cooperativas, aisladas también en el TIEMPO.
+
+### Añadido
+- `kernel/src/async_system/reloj.rs`: convierte la IRQ0 (PIT, 100 Hz) en una
+ primitiva asíncrona. `CONTADOR_PULSOS` (`AtomicU64`) y `EsperaFrame`, un
+ `Future` que se resuelve en el siguiente pulso — la unidad de cesión
+ cooperativa del userspace. Censo de wakers tras `Mutex`, drenado por la IRQ0.
+- `kernel/src/wasm`: ABI de fotograma. `AplicacionWasm` —instancia PERSISTENTE
+ entre fotogramas (`Store` + `TypedFunc<(), ()>` + región)— sustituye al
+ `wasm::ejecutar` de fuego-y-olvido. El módulo del userspace exporta ahora
+ `init()` (una vez) y `tick()` (un fotograma, y retorna).
+- Escudo de combustible: `Engine` con `Config::consume_fuel(true)` y
+ `CompilationMode::Eager` (el `fuel` mide solo ejecución). Presupuesto recargado
+ antes de cada `tick` — `FUEL_ARRANQUE` (20 M) y `FUEL_FOTOGRAMA` (2 M).
+- `kernel/src/wasm/env.rs`: capacidad `sys_render_frame` con **regiones de
+ dibujo** — cada fotograma se compone desplazado por el `(offset_x, offset_y)`
+ de la app; un tamaño ajeno a la región se rechaza. Canal de teclado **por
+ aplicación**: la IRQ1 difunde cada scancode a TODOS los canales, de modo que
+ varias apps reciben la entrada en paralelo sin robársela.
+- `kernel/src/grafico.rs`: `RegionPantalla` y `Color::DESALOJO` (púrpura).
+- `apps/discola/`: aplicación WASM construida para portarse mal — su `tick` es
+ un bucle cerrado. Demuestra el guardarrail de fuel en vivo.
+
+### Cambiado
+- `apps/hello_wasm`: migrada al ABI `init`/`tick`; su lienzo se dimensiona a la
+ región (480×560). El estado del cuadrado persiste en su memoria lineal.
+- `interrupts.rs`: la IRQ0 avanza el `reloj`; la IRQ1 difunde a los canales.
+- `consola.rs`: `volcar_marco` compone en una sub-región; `pintar_desalojo`
+ tatúa la baliza púrpura.
+- Contención de fallos: el subsistema WASM ya no usa `.expect()`. Toda falla
+ —carga, instanciación, desbordamiento, agotamiento de fuel— se devuelve como
+ `Result`; la tarea desaloja la app (baliza púrpura) y el kernel sigue vivo.
+
+### Verificado
+- QEMU: tres apps concurrentes. Dos instancias de `hello_wasm` (mismo bytecode,
+ regiones izquierda y derecha) renderizan y responden a W/A/S/D **en paralelo**,
+ con movimiento idéntico. La app díscola es desalojada en su primer fotograma
+ por agotamiento de combustible: su región queda púrpura y el sistema —kernel y
+ apps vecinas— no sufre un solo sobresalto.
+
+---
+
+## Fase 6.0 — Cuotas de memoria y ciclo de vida del userspace — 2026-05-22
+
+El aislamiento de las aplicaciones, que la Fase 5 hizo temporal (combustible),
+se completa ahora en la dimensión espacial (memoria) y se cierra la fuga de
+recursos del ciclo de vida.
+
+### Añadido
+- Techo de memoria lineal por aplicación: `4 MiB`. `wasm/mod.rs` construye un
+ `StoreLimits` (`StoreLimitsBuilder::memory_size` + `trap_on_grow_failure`) y lo
+ liga al `Store` con `Store::limiter`. Un `memory.grow` que rebase la cuota se
+ convierte en trampa; el kernel la captura y desaloja la app.
+- `FallaApp::SinMemoria` y `Color::DESALOJO_MEMORIA` (amarillo pálido). El
+ desalojo distingue la causa por color: púrpura (tiempo/aborto), amarillo
+ (memoria). La clasificación es robusta — `Error::as_trap_code()` da un código
+ público y unívoco: `TrapCode::GrowthOperationLimited`.
+- `Drop` para `AplicacionWasm`: al morir una app desalojada, su canal de teclado
+ se da de baja del censo de difusión de la IRQ1 — cierra la fuga señalada en la
+ Fase 5. `async_system::teclado` se reorganiza en `crear_canal` /
+ `registrar_canal` / `cerrar_canal`; el canal se inscribe al FINAL de la carga,
+ de modo que una carga fallida no deja canales huérfanos.
+- `apps/glotona/`: aplicación WASM construida para devorar memoria — su `tick`
+ invoca `memory.grow` sin freno. Demuestra el guardarrail espacial en vivo.
+
+### Verificado
+- QEMU: cuatro apps concurrentes. Las dos `hello_wasm` renderizan y responden al
+ teclado en paralelo; la app díscola es desalojada por combustible (franja
+ púrpura) y la app glotona por cuota de memoria (franja amarilla), ambas en su
+ primer fotograma, sin que el kernel ni las apps honradas se inmuten.
+
+---
+
+## Fase 6.1a — Sonda PCI y disco de pruebas — 2026-05-22
+
+Primer paso de la estrategia incremental hacia el almacenamiento: derribar el
+muro del descubrimiento de hardware antes de diseñar nada encima.
+
+### Añadido
+- `kernel/src/drivers/`: nuevo subsistema de drivers. `pci.rs` enumera el bus
+ PCI por fuerza bruta mediante el mecanismo de configuración #1 —puertos de E/S
+ `0xCF8` (dirección) y `0xCFC` (datos)—; recorre buses y dispositivos leyendo
+ el Vendor/Device ID, y localiza el disco virtio-blk (vendor `0x1AF4`, device
+ `0x1001`/`0x1042`), devolviendo su ubicación y sus seis BARs.
+- `kernel_main`: tras fundar la consola, sondea el bus PCI y deja constancia
+ visual del hallazgo.
+
+### Cambiado
+- `boot/src/main.rs`: forja un disco de pruebas `target/disk.img` (fichero
+ disperso de 32 MiB; se respeta si ya existe) y lo adjunta a QEMU como
+ `virtio-blk-pci`. **Corrección de plataforma:** la máquina `q35` es x86_64 y
+ exige la transmisión PCI; `virtio-blk-device` (su gemelo MMIO) es de ARM.
+
+### Verificado
+- QEMU: el kernel enumera el bus y reporta en pantalla
+ `virtio-blk en bus 0 dev 3 :: BAR0 E/S 0x6000` — un dispositivo virtio-blk
+ transicional (device `0x1001`), con su BAR0 en espacio de E/S. El muro del
+ descubrimiento PCI queda derribado; las cuatro apps del userspace siguen
+ operando sin alteración.
+
+---
+
+## Fase 6.1b — HAL, DMA y lectura del sector 0 — 2026-05-22
+
+Segundo paso del sustrato de almacenamiento: el diálogo real con el disco. El
+kernel monta el dispositivo virtio-blk y lee su primer sector por DMA.
+
+### Añadido
+- Dependencia `virtio-drivers` 1.13 (`no_std`, `default-features = false`,
+ feature `alloc`).
+- `kernel/src/drivers/disco.rs`:
+ - **Asignador de marcos** «bump» — reparte páginas físicas de 4 KiB para el
+ DMA, tomadas de la mayor región de RAM libre que el cargador reporta. No
+ libera: suficiente para una sonda (la gestión fina llegará con el grafo).
+ - **`KernelHal`** — implementa el `trait Hal` de `virtio-drivers`. `dma_alloc`
+ entrega marcos físicos a cero; `mmio_phys_to_virt` traduce los BARs (el
+ cargador mapea ≥ 4 GiB de memoria física, que cubre todo MMIO de PCI);
+ `share`/`unshare` usan un buffer rebote para que cualquier región del kernel
+ pueda viajar al dispositivo.
+ - **`montar_y_leer_sector0`** — enumera el bus, habilita E/S + memoria +
+ bus-master, monta el `PciTransport` y el `VirtIOBlk`, y lee el sector 0 por
+ **sondeo** del *used ring* (sin depender aún de interrupciones).
+- `kernel/src/drivers/pci.rs`: reescrito como `CamPuertos`, la implementación de
+ `ConfigurationAccess` de `virtio-drivers` sobre los puertos `0xCF8`/`0xCFC`.
+- `kernel_main` captura `physical_memory_offset` y la región de RAM de
+ `BootInfo`, funda el subsistema de disco y reporta el resultado de la sonda.
+
+### Cambiado
+- `boot/src/main.rs`: al forjar el disco de pruebas graba una firma
+ (`renaser-6.1b`) en su sector 0 — el testigo del viaje de ida y vuelta.
+
+### Verificado
+- QEMU: el kernel reporta `virtio-blk :: bus 0 dev 3 :: 65536 sectores ::
+ s0=renaser-6.1b`. La firma grabada por el anfitrión se lee de vuelta intacta:
+ descubrimiento PCI, transporte, DMA y transferencia funcionan de punta a
+ punta. Las cuatro apps del userspace siguen operando sin alteración.
+
+---
+
+## Fase 6.1c — El grafo de objetos direccionado por contenido — 2026-05-22
+
+Tercer y último paso del sustrato de almacenamiento. El disco deja de ser un
+dispositivo que se sondea y pasa a ser una MEMORIA QUE PERDURA: un grafo
+dirigido acíclico de objetos direccionados por contenido — no un sistema de
+archivos plano POSIX.
+
+### Añadido
+- `kernel/src/almacen.rs` — el grafo de objetos:
+ - **Objeto** — una carga útil de bytes (`datos`) y una lista de aristas
+ (`hijos`: hashes de otros objetos). Las aristas hacen del almacén un DAG.
+ - **Direccionamiento por contenido** — la identidad de un objeto es el hash
+ BLAKE3 de su forma serializada. De ello se siguen dos propiedades que un
+ sistema de archivos jamás regala: INTEGRIDAD (el contenido leído se
+ rehashea y se verifica contra el hash pedido) y DEDUPLICACIÓN (contenido
+ idéntico produce el mismo hash; se almacena una sola vez).
+ - **Disco como log** — el sector 0 es el superbloque (magia `RENASGRF`,
+ versión, cursor del log y hash de la raíz); tras él se anexan los registros
+ de objetos, `[longitud u32 LE][payload postcard][relleno a cero]`. Un
+ índice en memoria (hash -> sector) se reconstruye al arrancar recorriendo
+ el log de cabo a rabo.
+ - API: `init` —monta el disco, lee o forja el superbloque, reconstruye el
+ índice—, `almacenar`, `recuperar`, `raiz` y `fijar_raiz`.
+- Cinco capacidades nuevas del host en `wasm/env.rs` — `sys_object_put`,
+ `sys_object_datos`, `sys_object_hijo`, `sys_object_raiz` y
+ `sys_object_fijar_raiz` —, con la misma validación infranqueable de límites
+ de la memoria lineal que `sys_render_frame`. Distinguen dos clases de fallo:
+ un puntero inválido ABORTA la app —es su culpa, se traduce en trampa—; un
+ fallo del almacenamiento le devuelve un código de error negativo —no lo es—.
+- `apps/cronista/` — la primera aplicación del userspace que escribe en el
+ almacenamiento PERSISTENTE. En cada arranque consulta la raíz del grafo,
+ graba un objeto nuevo —`datos`: el número de arranque; `hijos`: la raíz
+ anterior, el eslabón del DAG— y lo corona como raíz. Pinta una celda por
+ arranque registrado y un testigo de integridad que recorre la cadena entera.
+- Dependencias: `serde` 1 y `postcard` 1 (serialización binaria compacta, el
+ formato que viaja al disco) y `blake3` 1 (la función hash). Las tres `no_std`.
+
+### Cambiado
+- `kernel/src/drivers/disco.rs` reescrito para un sustrato PERMANENTE:
+ - El asignador de marcos «bump» de la Fase 6.1b cede el paso a uno de MAPA DE
+ BITS con liberación real. `dma_dealloc` y `unshare` devuelven los marcos a
+ la arena: un almacén vivo, con su trasiego incesante de DMA, ya no la agota.
+ - El `VirtIOBlk` deja de montarse y destruirse en cada llamada: se monta UNA
+ vez y queda tras un `Mutex` global. `leer_sectores` / `escribir_sectores`
+ exponen la E/S de bloques — el disco deja de ser de solo lectura. Se retira
+ `montar_y_leer_sector0`, la sonda de un solo uso de la Fase 6.1b.
+- `boot/src/main.rs`: el disco de pruebas pasa a ser el disco de objetos. Ya
+ no se le graba una firma — se forja virgen, a cero, y el kernel lo formatea
+ la primera vez que no halle el superbloque.
+- `main.rs`: `informar_disco` (la sonda de la Fase 6.1b) se sustituye por
+ `informar_almacen`, que funda el grafo. El userspace pasa de cuatro a cinco
+ apps; las regiones de discola y glotona se reajustan para alojar a cronista.
+
+### Notas
+- **blake3 forzado a escalar.** El target del kernel corre sin SSE; un camino
+ SIMD de blake3 activado por detección en tiempo de ejecución ejecutaría
+ instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaría con un #UD. Se fija
+ blake3 con `pure` + los cuatro `no_*` (`sse2`, `sse41`, `avx2`, `avx512`):
+ implementación puramente escalar, sin SIMD ni ensamblador.
+
+### Verificado
+- QEMU, TRES arranques consecutivos sobre el mismo disco persistente: la app
+ cronista pinta 1, luego 2, luego 3 celdas — la cuenta de arranques sobrevive
+ a los reinicios porque vive en el grafo, en el disco, no en la RAM. El
+ testigo de integridad queda VERDE en los tres: el DAG entero se recorre, de
+ la raíz al primer eslabón, y su profundidad cuadra con la cuenta. El
+ superbloque en disco confirma la magia `RENASGRF`, versión 1, cursor 4 y la
+ raíz del tercer arranque; el registro del primer arranque guarda `datos = 1`
+ y cero hijos. Las otras cuatro apps siguen su curso sin alteración — las dos
+ `hello_wasm` renderizando, discola desalojada en púrpura, glotona en amarillo.
+
+---
+
+## Fase 6.2 — E/S de disco asíncrona por interrupción — 2026-05-22
+
+La Fase 6.1 hizo hablar al disco, pero por SONDEO: el procesador se quedaba en
+espera activa vigilando el *used ring* de virtio, incapaz de atender nada más.
+La 6.2 libera el planificador cooperativo — la E/S de bloques pasa a ser
+REACTIVA, guiada por la interrupción física del dispositivo.
+
+### Añadido
+- `EsperaDisco` (`drivers/disco.rs`): una transferencia de bloques expresada
+ como `Future` nativo. Posee sus buferes DMA —`BlkReq`, `BlkResp` y los datos,
+ en el heap para una dirección estable—; su `poll` envía la petición por la
+ API NO BLOQUEANTE de `virtio-drivers` (`read_blocks_nb`/`write_blocks_nb`),
+ consulta el *used ring* (`peek_used`) y, si la transferencia sigue en vuelo,
+ inscribe el waker y cede. `leer_bloques`/`escribir_bloques` lo construyen.
+- La IRQ del disco: `montar` descubre la línea de IRQ legada del dispositivo
+ (registro «Interrupt Line», offset 0x3C del espacio de configuración PCI),
+ registra un manejador en la IDT y abre la línea en el PIC.
+ `interrupts::irq_disco` → `disco::atender_irq` reconoce la interrupción en el
+ dispositivo —leer su registro ISR baja la línea INTx— y despierta, vía un
+ waker de ranura única, a la tarea que aguardaba el bloque.
+- `bloquear_en` — el puente para los contextos SÍNCRONOS (el arranque, las
+ capacidades WASM, que no pueden `.await`): lleva un `Future` de disco hasta
+ su final durmiendo la CPU con `hlt` —la despiertan la IRQ del disco o el
+ temporizador, como red de seguridad—; jamás en espera activa con el sistema
+ ya en marcha.
+- `pic::desenmascarar` / `pic::vector_irq`: abrir una línea concreta del par
+ 8259 (con su cascada, si vive en el esclavo) y mapear línea → vector de IDT.
+- `pci::linea_irq`: leer el registro «Interrupt Line» de un dispositivo.
+- `tarea_sonda_disco` (`main.rs`): una tarea del reactor que lee el sector 0 de
+ forma asíncrona — la prueba viva de que la IRQ conduce la E/S sin detener a
+ las aplicaciones.
+
+### Cambiado
+- `leer_sectores`/`escribir_sectores` se reescriben sobre la maquinaria
+ asíncrona (`bloquear_en` + `EsperaDisco`). El `almacen` y las capacidades
+ `sys_object_*` NO cambian una línea: heredan la E/S por interrupción de
+ forma transparente — la espera de disco deja de quemar ciclos en sondeo.
+- El `VirtIOBlk` pide al dispositivo que emita interrupciones al completar cada
+ petición (`enable_interrupts`).
+
+### Decisiones de ingeniería
+- **PIC, no IOAPIC.** El kernel corre íntegramente sobre el par 8259 (`pic.rs`)
+ y la crate `x86_64` no ofrece abstracción de APIC. Las IRQ legadas de PCI de
+ la máquina `q35` se enrutan por el 8259: basta leer la línea que el firmware
+ UEFI ya asignó y abrirla. Migrar al IOAPIC habría exigido levantar LAPIC +
+ IOAPIC + parseo de tablas ACPI — un subsistema entero, desproporcionado para
+ el objetivo. El resultado funcional es idéntico: E/S conducida por la
+ interrupción física.
+- **Las capacidades WASM no `.await`-ean.** El intérprete `wasmi` ejecuta las
+ funciones de host de forma SÍNCRONA: no hay manera de suspender un módulo a
+ mitad de una llamada de host. Por eso `sys_object_*` no se vuelven
+ asíncronas; usan `bloquear_en`, que duerme la CPU con `hlt` en lugar de
+ sondear. El verdadero solapamiento E/S ↔ render lo aprovechan las TAREAS del
+ reactor (`tarea_sonda_disco` hoy; la carga dinámica de módulos, mañana).
+- Una IRQ legada de PCI es de NIVEL: el manejador reconoce primero al
+ dispositivo —lo que baja su línea— y sólo después cierra el EOI del PIC; el
+ orden inverso reavivaría la interrupción en bucle.
+- Todo acceso al disco toma su `Mutex` con las interrupciones desactivadas
+ (`without_interrupts`): la IRQ del disco jamás encuentra el cerrojo ocupado,
+ lo que hace imposible el interbloqueo por interrupción.
+
+### Verificado
+- QEMU: el disco virtio-blk se enruta a la IRQ 11. La `tarea_sonda_disco`
+ reporta «sonda asíncrona OK -- 2 IRQ de disco atendidas»: la interrupción del
+ disco dispara de verdad. La app cronista, sobre el disco persistente heredado
+ de la Fase 6.1c, continúa la cuenta de arranques (3 → 4 → 5 celdas) con el
+ testigo de integridad del DAG en verde — la nueva E/S asíncrona lee y escribe
+ el grafo sin un solo fallo. Las cuatro apps WASM siguen su curso, sin un
+ sobresalto ni un micro-congelamiento.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
new file mode 100644
index 0000000..2400deb
--- /dev/null
+++ b/renaser/CLAUDE.md
@@ -0,0 +1,88 @@
+# renaser — guía operativa
+
+**renaser** es un kernel asíncrono de *Espacio de Direccionamiento Único*
+(SASOS) en Rust `#![no_std]`, bare-metal x86_64. Rompe con POSIX: interfaz
+visual nativa desde el arranque, aislamiento **por software** (no por MMU),
+el texto como caso particular del dibujo.
+
+Documentos hermanos: `ARCHITECTURE.md` (arquitectura), `ROADMAP.md` (fases y
+plan), `CHANGELOG.md` (registro técnico de cambios), `DIARIO.md` (crónica en
+lenguaje llano), `README.md` (presentación).
+
+## Construir y ejecutar
+
+Desde la raíz del repositorio:
+
+| Comando | Efecto |
+|---|---|
+| `cargo run` | compila el kernel, forja la imagen UEFI y abre QEMU |
+| `cargo build` | compila el kernel + la imagen, sin lanzar QEMU |
+| `cargo kernel` | alias: compila solo el kernel para `x86_64-unknown-none` |
+
+Verificación headless (sin ventana): añadir
+`-- -display none -monitor unix:/tmp/q.sock,server,nowait` y capturar con
+`screendump` por el socket del monitor.
+
+Reconstruir la app WASM del userspace tras tocar `apps/hello_wasm`:
+
+```sh
+cd apps/hello_wasm
+cargo build --target wasm32-unknown-unknown --release
+cp target/wasm32-unknown-unknown/release/hello_wasm.wasm ../../kernel/assets/app.wasm
+```
+
+## Estructura del espacio de trabajo
+
+- `boot/` — orquestador de **anfitrión**: construye la imagen de disco UEFI con
+ la crate `bootloader` 0.11 y lanza QEMU. Es el **único miembro** del workspace.
+- `kernel/` — el kernel **bare-metal**. Está **excluido** del workspace y se
+ compila solo como dependencia de artefacto de `boot` (target
+ `x86_64-unknown-none`). NUNCA debe ser miembro del workspace ni compilarse
+ para el anfitrión: lo haría fallar por un `_start` duplicado.
+- `apps/` — aplicaciones del userspace, módulos `.wasm`
+ (target `wasm32-unknown-unknown`). Workspaces propios, excluidos.
+
+Módulos del kernel (`kernel/src/`): `main`, `grafico`, `consola`, `baliza`,
+`sync`, `gdt`, `interrupts`, `pic`, `drivers/`, `almacen`, `memory/`,
+`async_system/`, `texto`, `wasm/`. El detalle de cada uno está en
+`ARCHITECTURE.md`.
+
+## Toolchain
+
+Nightly, fijado en `rust-toolchain.toml`. Targets `x86_64-unknown-none` y
+`wasm32-unknown-unknown`. Componentes `rust-src` y `llvm-tools` (los exige el
+build script de la crate `bootloader`). Entorno verificado: Artix Linux,
+QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
+
+## Convenciones
+
+- **Idioma:** comentarios E identificadores en español.
+- **`unsafe`:** confinado en células mínimas, envuelto de inmediato en una
+ abstracción segura. `#![deny(unsafe_op_in_unsafe_fn)]`. Cada bloque `unsafe`
+ lleva un comentario `SEGURIDAD:` que justifica su corrección.
+- **Framebuffer / MMIO:** siempre `core::ptr::write_volatile`; jamás asignación
+ indexada directa (el optimizador la elidiría).
+- **`no_std` estricto:** solo `core::*` y `alloc::*` (heap vivo desde la Fase 3).
+ Prohibido cualquier ruta de `std::*`.
+- **Alineación:** 16 bytes para estructuras genéricas; 4096 bytes (página) para
+ buffers de memoria y regiones de asignador.
+- Código densamente comentado; se cuida la coherencia de estilo entre módulos.
+
+## Estado
+
+Fases 1 a 5, la 6.0, la 6.1 completa (sustrato de almacenamiento: sonda PCI,
+HAL/DMA y el grafo de objetos) y la 6.2 (E/S de disco asíncrona por
+interrupción) completadas y verificadas en QEMU. Ver `ROADMAP.md`.
+
+## Flujo de trabajo
+
+En **cada iteración** de trabajo, sin excepción:
+
+1. Actualizar `CHANGELOG.md` — la entrada técnica detallada del cambio.
+2. Actualizar `DIARIO.md` — la misma jornada contada en lenguaje llano y
+ elegante, sin carga técnica (es una crónica humana, no un registro técnico).
+3. Verificar en QEMU si el cambio es observable (con captura de pantalla).
+4. `git commit` (mensaje en español, descriptivo) y `git push origin main`.
+
+El remoto `origin` es `gitea.gioser.net/sergio/renaser`. Mensajes de commit en
+español. Verifica una fase en QEMU antes de darla por cerrada.
diff --git a/renaser/Cargo.lock b/renaser/Cargo.lock
new file mode 100644
index 0000000..e1e1cd1
--- /dev/null
+++ b/renaser/Cargo.lock
@@ -0,0 +1,1178 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "az"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures",
+]
+
+[[package]]
+name = "boot"
+version = "0.1.0"
+dependencies = [
+ "bootloader",
+ "kernel",
+]
+
+[[package]]
+name = "bootloader"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5fb69232f250c0d5b74eeb285702b4b0479f574980f45dc9bef97b2c65e2021"
+dependencies = [
+ "anyhow",
+ "bootloader-boot-config",
+ "fatfs",
+ "gpt",
+ "llvm-tools",
+ "mbrman",
+ "serde_json",
+ "tempfile",
+]
+
+[[package]]
+name = "bootloader-boot-config"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a5e475c13d3d988e105b76b92aeea94614c12590c33495165c02b16e895375"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bootloader_api"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c074a3d1075b26ed77e502d6af8ab1263abbdc3d026c166e46480f973287293"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cc"
+version = "1.2.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "embedded-graphics"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe"
+dependencies = [
+ "az",
+ "byteorder",
+ "embedded-graphics-core",
+ "float-cmp",
+ "micromath",
+]
+
+[[package]]
+name = "embedded-graphics-core"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f"
+dependencies = [
+ "az",
+ "byteorder",
+]
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "enumn"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fatfs"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0"
+dependencies = [
+ "bitflags 1.3.2",
+ "byteorder",
+ "log",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fontdue"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
+dependencies = [
+ "hashbrown 0.15.5",
+ "ttf-parser",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gpt"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69"
+dependencies = [
+ "bitflags 2.11.1",
+ "crc",
+ "log",
+ "uuid",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kernel"
+version = "0.1.0"
+dependencies = [
+ "blake3",
+ "bootloader_api",
+ "crossbeam-queue",
+ "embedded-graphics",
+ "fontdue",
+ "futures-util",
+ "linked_list_allocator",
+ "mirada-layout",
+ "postcard",
+ "serde",
+ "spin",
+ "virtio-drivers",
+ "wasmi",
+ "x86_64",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "linked_list_allocator"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897"
+dependencies = [
+ "spinning_top",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "llvm-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "mbrman"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1fc3bff63c208d4a14301c6cb807af2d1a0760052584ce3f9a737b55fb85498"
+dependencies = [
+ "bincode",
+ "bitvec",
+ "serde",
+ "serde-big-array",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "micromath"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
+
+[[package]]
+name = "mirada-layout"
+version = "0.1.0"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "safe-mmio"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4813ee49326057f105d6d8ca3d8f9265095f26aa7b42094e487028403a594f4c"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-big-array"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spinning_top"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "string-interner"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "getrandom",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "virtio-drivers"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfdc1c628cdd8ce7c3b9e65a8ed550d0338e9ef9f911e729666f1cce097de2f7"
+dependencies = [
+ "bitflags 2.11.1",
+ "enumn",
+ "log",
+ "safe-mmio",
+ "thiserror 2.0.18",
+ "zerocopy",
+]
+
+[[package]]
+name = "volatile"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "wasmi"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22bf475363d09d960b48275c4ea9403051add498a9d80c64dbc91edabab9d1d0"
+dependencies = [
+ "spin",
+ "wasmi_collections",
+ "wasmi_core",
+ "wasmi_ir",
+ "wasmparser 0.228.0",
+]
+
+[[package]]
+name = "wasmi_collections"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85851acbdffd675a9b699b3590406a1d37fc1e1fd073743c7c9cf47c59caacba"
+dependencies = [
+ "hashbrown 0.15.5",
+ "string-interner",
+]
+
+[[package]]
+name = "wasmi_core"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef64cf60195d1f937dbaed592a5afce3e6d86868fb8070c5255bc41539d68f9d"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "wasmi_ir"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dcb572ce4400e06b5475819f3d6b9048513efbca785f0b9ef3a41747f944fd8"
+dependencies = [
+ "wasmi_core",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.228.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.1",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser 0.244.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "x86_64"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7841fa0098ceb15c567d93d3fae292c49e10a7662b4936d5f6a9728594555ba"
+dependencies = [
+ "bit_field",
+ "bitflags 2.11.1",
+ "const_fn",
+ "rustversion",
+ "volatile",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/renaser/Cargo.toml b/renaser/Cargo.toml
new file mode 100644
index 0000000..f014b24
--- /dev/null
+++ b/renaser/Cargo.toml
@@ -0,0 +1,89 @@
+# =============================================================================
+# renaser :: manifiesto raíz del espacio de trabajo
+# -----------------------------------------------------------------------------
+# Un único espacio de direccionamiento, una única verdad de versiones.
+# Las dependencias se declaran aqui una sola vez y persisten, coherentes,
+# a traves de todos los miembros del sistema.
+# =============================================================================
+
+[workspace]
+resolver = "2"
+# Solo `boot` es miembro del espacio de trabajo: corre en el anfitrion. El
+# kernel queda EXCLUIDO a proposito —es bare-metal puro y jamas debe compilarse
+# para el anfitrion— y se construye unica y exclusivamente como dependencia de
+# artefacto de `boot`, que le fija el target `x86_64-unknown-none`.
+members = ["boot"]
+# El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte,
+# cada cual con su propio target; quedan fuera del espacio de trabajo.
+exclude = ["kernel", "apps"]
+
+# -----------------------------------------------------------------------------
+# Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`.
+# -----------------------------------------------------------------------------
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+authors = ["JL Soltech <gerencia@jlsoltech.com>"]
+description = "renaser :: kernel asincrono SASOS basado en aislamiento por software"
+
+# -----------------------------------------------------------------------------
+# Dependencias del ecosistema. Reutilizamos infraestructura madura en lugar
+# de reinventar boilerplate. `default-features = false` se aplica con bisturi,
+# unicamente alli donde las features por defecto arrastrarian `std`.
+# -----------------------------------------------------------------------------
+[workspace.dependencies]
+# Constructor de la imagen de disco UEFI/BIOS. Es una herramienta de ANFITRION
+# (usa std), la consume el miembro `boot` y NO debe enlazarse jamas en el kernel.
+bootloader = "0.11"
+
+# API `#![no_std]` que el kernel SI consume: expone `entry_point!`, `BootInfo`
+# y la descripcion del framebuffer GOP. Su version debe acompasar a `bootloader`.
+bootloader_api = "0.11"
+
+# Abstraccion de la CPU x86_64: registros de control, GDT, IDT e instrucciones
+# privilegiadas sin ensamblador inline. Es `#![no_std]` de nacimiento; su
+# feature `instructions` (activa por defecto) nos da `hlt` y el control de IRQ.
+x86_64 = "0.15"
+
+# Rasterizacion de primitivas vectoriales sobre el framebuffer. `#![no_std]`,
+# sin heap: en la Fase 1 solo aporta el rasgo `DrawTarget` del lienzo.
+embedded-graphics = "0.8"
+
+# Rasterizador de tipografias. REQUIERE `alloc`, de modo que solo se teje en el
+# kernel una vez exista el asignador (Fase 2). `default-features = false`
+# silencia su feature `std`. Se declara ya para congelar la version.
+# `hashbrown` es lo que ACTIVA el modo `no_std` de fontdue; sin el, la crate
+# recae en `std::collections` y no compila para bare-metal. `simd` queda fuera:
+# nuestro target no tiene SSE.
+fontdue = { version = "0.9", default-features = false, features = ["hashbrown"] }
+
+# --- Fase 3 :: heap dinamico y reactor asincrono ---
+# Asignador del heap del kernel — algoritmo probado, configurado `no_std`.
+linked_list_allocator = "0.10"
+# Exclusion mutua sin sistema operativo: `Mutex` e inicializacion unica `Once`.
+spin = "0.9"
+# Cola lock-free, segura frente a interrupciones, para el canal de scancodes.
+crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] }
+# Combinadores `Future`/`Stream` y `AtomicWaker`. Sin ejecutor ni I/O: solo
+# las piezas que renaser teje en su propio reactor.
+futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
+
+# --- Fase 4 :: interprete WebAssembly para el userspace aislado ---
+# `wasmi` en modo bare-metal: sin `std`, sin `wat`, sin `simd` (el target no
+# tiene SSE). `hash-collections` le da mapas basados en hashbrown, `no_std`.
+wasmi = { version = "1.0", default-features = false, features = ["hash-collections"] }
+
+# -----------------------------------------------------------------------------
+# Perfiles de compilacion. Cargo exige que los perfiles vivan en la RAIZ del
+# espacio de trabajo. `panic = "abort"` es obligatorio: sin sistema operativo
+# subyacente no hay desenrollado de pila (`unwinding`) posible.
+# -----------------------------------------------------------------------------
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s" # codigo compacto: cada byte cuenta en bare-metal
+lto = true # el optimizador cruza fronteras de crate
+codegen-units = 1 # una sola unidad => maxima oportunidad de optimizacion
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
new file mode 100644
index 0000000..64bb2cc
--- /dev/null
+++ b/renaser/DIARIO.md
@@ -0,0 +1,226 @@
+# Diario de renaser
+
+*Una crónica, en lenguaje llano, de cómo va naciendo un sistema operativo.
+Cada jornada de trabajo añade aquí su página.*
+
+---
+
+## El primer día — la luz
+
+Todo empezó con un gesto pequeño y luminoso: lograr que la máquina, al
+despertar, no mostrara la fría consola de texto de siempre, sino una superficie
+limpia y serena, de un azul profundo, casi nocturno. Puede parecer poca cosa,
+pero esa superficie es el cimiento de cuanto vendrá: el lienzo en blanco de un
+sistema entero.
+
+Junto a esa primera luz nació también una promesa de honestidad. Si alguna vez
+algo saliera terriblemente mal, renaser no se quedaría callado: pintaría una
+franja roja, ancha y visible, para confesarlo sin rodeos. Una alarma sincera.
+
+## El empaquetado — un cuerpo para viajar
+
+Un sistema no sirve de nada si no puede encenderse en una máquina de verdad.
+Así que el segundo paso fue darle a renaser un cuerpo: envolverlo en un disco
+capaz de arrancar por sí solo, y enseñar al ordenador a lanzarlo. Desde
+entonces, una sola orden basta para verlo cobrar vida ante nuestros ojos.
+
+## Los reflejos — aprender a reaccionar
+
+Hasta aquí, renaser sabía mostrarse, pero no sabía protegerse. La tercera etapa
+consistió en darle reflejos: la capacidad de reaccionar ante lo inesperado en
+lugar de derrumbarse en silencio. Se le tejió una red de seguridad interna para
+que, ante un tropiezo, supiera recogerse con dignidad y avisar de lo ocurrido.
+
+## El latido — sentir el tiempo y escuchar
+
+Un sistema vivo necesita pulso. Se le dio a renaser un latido regular, como un
+metrónomo interno, y oídos para escuchar el teclado. Por primera vez el sistema
+dejó de estar simplemente quieto: empezó a respirar, a redibujarse con cada
+latido y a notar cuándo una mano pulsaba una tecla.
+
+## La memoria y la fluidez — crecer y atender muchas cosas
+
+Para crecer, renaser necesitaba memoria de la que disponer con libertad. Y para
+no atascarse, necesitaba aprender a atender varias cosas a la vez sin que unas
+estorbaran a otras. En esta etapa recibió ambas. Además aprendió a escribir de
+verdad: con una tipografía elegante, dibujando cada letra con esmero. El texto
+dejó de ser un molde rígido para volverse, también él, un dibujo.
+
+## La habitación segura — invitar a otros programas
+
+La etapa más ambiciosa hasta ahora: abrir la puerta a que otros programas
+vivieran dentro de renaser. Pero no de cualquier modo. Cada programa invitado
+entra en una habitación sellada de la que no puede salir; solo puede hacer
+aquello para lo que renaser, expresamente, le tiende un puente. Todo lo demás,
+sencillamente, no existe para él. La primera visita fue modesta y entrañable:
+un cuadrado de color que se desliza por la pantalla obedeciendo al teclado.
+
+## Una jornada de orden — ordenar la casa y guardar la historia
+
+No todo es construir; a veces toca ordenar. Se revisó la estructura del
+proyecto y se repartió mejor el trabajo entre sus piezas, para que cada una
+tuviera un cometido claro y nítido. Se escribió la documentación que cuenta qué
+es renaser y hacia dónde camina. Y se guardó todo el trabajo en un lugar
+seguro, donde queda registro fiel de cada cambio, de modo que ninguna jornada
+se pierda. Hubo, además, un pequeño susto: una contraseña se coló por error
+donde no debía; se retiró de inmediato y se dejó constancia para corregirla.
+
+## El reparto del tiempo — muchos huéspedes, ningún tirano
+
+Hasta esta jornada, el programa invitado de renaser, una vez dentro, se quedaba
+con la casa entera: hablaba sin pausa y no dejaba sitio a nadie más. Era un solo
+huésped, y se comportaba como dueño.
+
+Esta etapa enseñó a renaser a repartir el tiempo. Se acordó un compás —el latido
+del sistema marca, una y otra vez, el turno de cada cual— y se acordó también un
+gesto de cortesía: cada programa hace su pequeña tarea, pinta su fotograma y, al
+terminar, cede el paso. Así, lo que antes era un único inquilino pasaron a ser
+varios, conviviendo en paz, cada uno en su propia ventana de la pantalla.
+
+Pero la cortesía, por sí sola, es frágil: basta un huésped maleducado —uno que
+no quiera ceder nunca el turno— para paralizarlo todo. Por eso se añadió una
+salvaguarda serena pero firme: a cada programa se le entrega, en cada turno, una
+ración medida de tiempo. Si la agota sin terminar —porque se ha enredado en un
+bucle sin fin—, renaser le retira el turno con delicadeza, tiñe su ventana de un
+púrpura inconfundible y prosigue. El sistema no se cuelga; simplemente despide al
+inquilino díscola y sigue atendiendo a los demás, que ni se enteran.
+
+Para comprobarlo se invitó, a propósito, a un huésped díscola: un programa hecho
+para enredarse en un bucle eterno. Al arrancar, las dos ventanas honradas
+cobraron vida y obedecieron al teclado a la vez, en perfecta armonía; la tercera,
+la del díscola, se apagó al instante en un púrpura tranquilo. La promesa quedó
+demostrada: renaser es veloz porque confía, pero nunca ingenuo.
+
+## La otra mitad del muro — el espacio, no solo el tiempo
+
+La jornada anterior enseñó a renaser a repartir el tiempo y a contener al que no
+quería cederlo. Pero un huésped puede portarse mal de dos maneras: acaparando el
+tiempo, sí, o acaparando el espacio. Quedaba media casa por proteger.
+
+En esta etapa se le puso a cada programa un techo a la memoria que puede
+reclamar. Mientras se conforme con su habitación, vive tranquilo; si pretende
+derribar las paredes para anexarse la casa entera, renaser se lo impide con
+serenidad y, como con el díscola, lo despide —esta vez tiñendo su ventana de un
+amarillo pálido, para que de un vistazo se distinga al que abusó del espacio del
+que abusó del tiempo.
+
+Se cerró además una pequeña negligencia de la jornada anterior: cuando un
+huésped era despedido, renaser olvidaba recoger del todo sus cosas. Ahora, al
+marcharse cualquier programa, el sistema reclama hasta el último de sus enseres;
+no queda rastro. Y se invitó a un nuevo huésped de prueba, una aplicación
+glotona hecha para devorar memoria: al arrancar, fue frenada y despedida al
+instante, su ventana amarilla junto a la púrpura del díscola, mientras las dos
+aplicaciones honradas seguían su baile, ajenas a todo.
+
+Con esto, las dos dimensiones físicas —el tiempo y el espacio— quedan bajo el
+gobierno firme y sereno de renaser. El siguiente horizonte es más hondo: dar al
+sistema una memoria que perdure, un lugar donde guardar las cosas más allá del
+apagado. Pero esa es ya otra página.
+
+## El primer golpe a la roca — encontrar el disco
+
+Dar a renaser una memoria que perdure exige, antes que nada, hablar con un
+disco. Y un disco, a diferencia de la pantalla o del teclado, no se le anuncia
+al sistema al nacer: hay que salir a buscarlo. Es una empresa grande, así que se
+decidió acometerla con prudencia, a pasos cortos y firmes.
+
+El primer paso fue, sencillamente, encontrar el disco. renaser aprendió a
+recorrer el bus por el que se enganchan los periféricos de la máquina,
+preguntando puerta por puerta «¿quién hay aquí?», hasta dar con el disco virtual
+que se le había preparado. Y al hallarlo, lo dijo en voz alta sobre su propia
+pantalla: lo había localizado, y supo decir en qué dirección vivía.
+
+Puede parecer un gesto pequeño —una línea de texto—, pero es el primer golpe de
+pico contra una roca dura. La conversación con el disco apenas empieza; pero la
+puerta, al menos, ya está encontrada.
+
+## La primera palabra del disco — leer lo que se escribió
+
+Encontrada la puerta, faltaba lo más difícil: cruzarla. Hablar de verdad con un
+disco no es pedirle las cosas y esperar: es tender con él una memoria
+compartida, un terreno común donde el sistema deja una petición y el disco
+deposita su respuesta. Montar ese terreno —y aprender el protocolo para usarlo—
+fue el trabajo de esta jornada, el más cercano al metal de cuantos renaser ha
+acometido.
+
+Para no equivocar el paso se ideó una prueba sencilla y honesta. Al preparar el
+disco, el sistema anfitrión grabó en su primer renglón una breve firma, una
+palabra clave. Y entonces se le pidió a renaser que, ya por sus propios medios,
+abriera el disco y leyera ese mismo renglón. Si la palabra volvía intacta, no
+habría duda: el camino completo —encontrar el disco, tender la memoria
+compartida, pedir, esperar y recibir— funcionaba de cabo a rabo.
+
+Al arrancar, renaser lo dijo en su pantalla, sereno: había leído el sector
+cero, y la palabra que mostró era, letra por letra, la que se había grabado. El
+disco había hablado, y renaser lo había entendido. La roca, por fin, cedió.
+
+Queda por delante lo más hermoso: sobre esta conversación recién abierta,
+levantar una verdadera memoria duradera —no un archivo de los de antes, sino un
+tejido de objetos—. Pero el cimiento ya está puesto, y es firme.
+
+## Un tejido de objetos — la memoria que perdura
+
+La conversación con el disco estaba abierta; faltaba decidir qué contarle. El
+mundo que renaser hereda de la informática de siempre habría respondido sin
+pensarlo: archivos, carpetas, rutas con barras. renaser eligió otra cosa.
+
+En lugar de un archivero de cajones con etiquetas, se tejió un grafo de
+objetos. Cada objeto es un puñado de datos y unos cuantos hilos que lo enlazan
+con otros. Y —esta es la idea hermosa— ningún objeto lleva un nombre que
+alguien le haya puesto: su nombre es su contenido. De cada uno se calcula una
+huella única, irrepetible, y esa huella es su identidad. Dos cosas idénticas
+tienen la misma huella, de modo que nada se guarda dos veces; y si una se
+corrompe, su huella deja de cuadrar y la mentira se delata sola.
+
+Para que esa memoria mereciera el nombre de duradera hubo que pulir el trato
+con el disco: enseñarle a renaser no solo a leer, sino a escribir. Y poner
+orden en la despensa de la que el sistema toma prestada memoria para hablar con
+el hardware — que ahora, al terminar cada gestión, devuelve lo que tomó en vez
+de acumularlo sin fin.
+
+Y entonces llegó la prueba más bonita. Se escribió una pequeña aplicación, una
+cronista, con un único oficio: llevar la cuenta de las veces que el sistema
+despierta. En cada arranque consulta el grafo, encuentra el último apunte,
+añade el suyo —enlazado al anterior, como el eslabón nuevo de una cadena— y
+pinta una casilla por cada despertar registrado.
+
+Se apagó la máquina y se volvió a encender. Y otra vez. La cronista dibujó
+primero una casilla, luego dos, luego tres. La cuenta no se perdía con el
+apagón: vivía en el disco, en el tejido de objetos, y cada reinicio la
+encontraba intacta y la hacía crecer. Por primera vez, renaser recordaba algo
+de una vida a la siguiente.
+
+La memoria duradera ya no es una promesa. Es un tejido, y tiene sus primeros
+hilos.
+
+## El disco que avisa — la espera que ya no congela
+
+Hablar con el disco, hasta esta jornada, tenía un precio oculto. Cada vez que
+renaser le pedía un bloque, el sistema entero contenía el aliento: el procesador
+se quedaba mirando fijamente al disco, preguntando una y otra vez «¿ya?, ¿ya?»,
+sin hacer nada más, hasta que la respuesta llegaba. Para una lectura suelta
+apenas se notaba. Pero el futuro de renaser —cargar aplicaciones enteras desde
+el disco— habría convertido esos instantes en tirones visibles, en pequeños
+congelamientos de la imagen.
+
+Así que se le enseñó al disco a AVISAR. En lugar de que el sistema vigile, ahora
+es el disco quien, al terminar, da un golpecito en el hombro del kernel —una
+interrupción— para decir «listo». Y mientras tanto, renaser no aguarda de brazos
+cruzados: si hay trabajo, lo hace; si no, se adormece un instante, y el propio
+golpecito del disco lo despierta. Ni un ciclo malgastado.
+
+Para no equivocar el paso se escribió una pequeña sonda: una tarea que pide al
+disco su primer bloque y, en vez de quedarse esperando, cede el turno. Las
+aplicaciones de la pantalla siguieron pintándose, fluidas, mientras el disco
+trabajaba por su cuenta; y cuando el bloque estuvo listo, su aviso reanudó la
+sonda. En la pantalla quedó escrito: la lectura se había atendido por aviso, no
+por vigilancia.
+
+Es un cambio que casi no se ve —la cronista sigue contando arranques, una
+casilla más cada vez— pero que se sentirá en todo lo que venga después. El disco
+y renaser ya no se miran fijamente: se hablan, y entre frase y frase, cada cual
+atiende lo suyo.
+
+---
+
+*El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/README.md b/renaser/README.md
new file mode 100644
index 0000000..12a6c3b
--- /dev/null
+++ b/renaser/README.md
@@ -0,0 +1,48 @@
+# renaser
+
+**renaser** es un kernel asíncrono de *Espacio de Direccionamiento Único*
+(SASOS), escrito en Rust `#![no_std]` para x86_64 bare-metal.
+
+Es un sistema operativo disruptivo que rompe por completo con el paradigma
+POSIX de los años 70: no emula Linux, no usa archivos planos, no usa TTYs ni
+capas GNU. El aislamiento entre aplicaciones no descansa en la MMU ni en los
+anillos de privilegio de la CPU, sino en **límites matemáticos sobre el
+bytecode** — aislamiento por software (SFI). La interfaz es visual desde el
+primer microsegundo: el texto es, simplemente, un caso particular del dibujo.
+
+## Qué hace, hoy
+
+- Arranca por **UEFI** y adopta el framebuffer GOP con doble búfer sin parpadeo.
+- Se **autoempaqueta** en una imagen de disco UEFI y se lanza en QEMU.
+- Tiene **reflejos de fallo**: GDT/TSS, IDT y manejadores de excepción; si
+ colapsa, lo dibuja (franja roja de pánico, naranja de memoria agotada).
+- **Late con el hardware**: PIC remapeado, temporizador (PIT) y teclado.
+- Gestiona **memoria dinámica** (heap de 64 MiB, asignador global).
+- Ejecuta un **reactor asíncrono cooperativo** sobre los `Future` nativos de
+ Rust: las interrupciones no conmutan contexto, despiertan tareas.
+- Rasteriza **texto vectorial** al vuelo con `fontdue`.
+- Ejecuta un **userspace WebAssembly** aislado por capacidades (`wasmi`): las
+ aplicaciones solo tocan el mundo a través de funciones de host concedidas.
+
+## Construir y ejecutar
+
+Requisitos: `rustup` con toolchain nightly, QEMU y firmware OVMF.
+
+```sh
+cargo run
+```
+
+Compila el kernel para `x86_64-unknown-none`, forja la imagen de disco UEFI y
+abre QEMU. Ver `CLAUDE.md` para el resto de comandos y el flujo de la app WASM.
+
+## Documentación
+
+| Documento | Contenido |
+|---|---|
+| `ARCHITECTURE.md` | la arquitectura del sistema, subsistema a subsistema |
+| `ROADMAP.md` | fases completadas y plan de las siguientes |
+| `CLAUDE.md` | guía operativa: comandos, estructura y convenciones |
+
+## Licencia
+
+MPL-2.0
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
new file mode 100644
index 0000000..c00988c
--- /dev/null
+++ b/renaser/ROADMAP.md
@@ -0,0 +1,108 @@
+# renaser — Hoja de ruta
+
+Estado de las fases del proyecto. Para la arquitectura, ver `ARCHITECTURE.md`.
+
+## Fases completadas
+
+Todas verificadas en QEMU (captura de pantalla incluida en su momento).
+
+### Fase 1 — el primer microsegundo
+Arranque UEFI, adopción del framebuffer GOP, lienzo de doble búfer y la baliza
+de pánico (franja roja al colapsar). Punto de entrada con `bootloader_api`.
+
+### Fase 1.5 — empaquetado y arranque
+Miembro `boot/`: constructor de la imagen de disco UEFI (crate `bootloader`
+0.11, vía dependencia de artefacto) y lanzador de QEMU. `cargo run` pasa a
+compilar, forjar la imagen y arrancar, todo de un gesto.
+
+### Fase 2.0 — cimientos del manejo de fallos
+GDT propia + TSS con stack de emergencia (IST) para el doble fallo. IDT con
+manejadores de excepción de CPU. El breakpoint es recuperable; el resto de
+excepciones encienden la baliza.
+
+### Fase 2.1 — interrupciones de hardware
+Remapeo del PIC 8259 fuera del rango de las excepciones. Temporizador (PIT) a
+100 Hz e IRQ1 de teclado. El kernel pasa de "pintar una vez" a un bucle de
+render despertado por el hardware.
+
+### Fase 3 — memoria dinámica y reactor asíncrono
+Heap de 64 MiB con `linked_list_allocator` como asignador global; manejador
+propio de OOM (franja naranja). Reactor cooperativo: `Executor`, `Task`,
+`Waker`. Texto vectorial con `fontdue`: el texto deja de ser mapa de bits.
+
+### Fase 4 — el escudo de aislamiento WASM
+Runtime `wasmi` `no_std`. Matriz de capacidades con dos funciones de host
+(`sys_render_frame`, `sys_get_scancode`) y validación infranqueable de límites
+de la memoria lineal. Primera app del userspace: `apps/hello_wasm`, un módulo
+`wasm32` aislado que pinta y responde al teclado.
+
+### Fase 5 — multitarea cooperativa, fuel y reloj
+Unificación del reactor (Fase 3) y el runtime WASM (Fase 4). El ABI del
+userspace pasa a `init`/`tick`: cada `tick` es un punto de cesión cooperativa.
+`async_system::reloj` convierte la IRQ0 en el `Future` `EsperaFrame`, que marca
+el compás de los fotogramas. Cada app es una `AplicacionWasm` persistente y una
+tarea del reactor. Escudo de combustible (`fuel`): cada `tick` corre con un
+presupuesto estricto; agotarlo desaloja la app sin tocar al kernel. Capacidades
+ampliadas — regiones de dibujo por app y canal de teclado por app. Verificado
+en QEMU con tres apps concurrentes, una de ellas díscola y desalojada en vivo.
+
+### Fase 6.0 — cuotas de memoria y ciclo de vida
+Completa el aislamiento espacial del userspace. Cada `AplicacionWasm` instancia
+su `Store` con un `StoreLimits` (techo de memoria lineal de 4 MiB, vía
+`Store::limiter`); rebasarlo es una trampa que desaloja la app con baliza
+amarilla — gemela de la púrpura del desalojo por combustible. `Drop` para
+`AplicacionWasm` reconcilia el ciclo de vida: da de baja el canal de teclado de
+la difusión de la IRQ1. Verificado en QEMU con cuatro apps — una díscola
+(desalojo temporal) y una glotona (desalojo espacial), ambas en vivo.
+
+## Fase 6.1 — sustrato de almacenamiento (completada)
+
+Estrategia incremental para el almacenamiento, frente al riesgo del hardware:
+
+- **6.1a — Sonda PCI** *(hecha)* — `drivers/pci.rs` enumera el bus PCI por
+ `0xCF8`/`0xCFC` y localiza el disco virtio-blk; `boot` forja el disco de
+ pruebas y lo adjunta como `virtio-blk-pci`. El muro del descubrimiento de
+ hardware queda derribado.
+- **6.1b — HAL y lectura de sector** *(hecha)* — `drivers/disco.rs`: asignador
+ de marcos «bump», `KernelHal` (el `trait Hal` de `virtio-drivers`: DMA y
+ traducción de direcciones) y `montar_y_leer_sector0`, que lee el sector 0 por
+ sondeo. Verificado por una firma que viaja del anfitrión al disco y vuelve.
+- **6.1c — Grafo de objetos** *(hecha)* — `almacen.rs`: el almacenamiento como
+ DAG direccionado por contenido —la identidad de un objeto es el hash BLAKE3
+ de su forma serializada (`postcard`); el disco se organiza como un log con
+ superbloque e índice—, y las cinco capacidades `sys_object_*` que lo exponen
+ al userspace. `drivers/disco.rs` gana un asignador de marcos con liberación
+ real (mapa de bits), escritura de sectores y un `VirtIOBlk` persistente. La
+ app `cronista` lleva la cuenta de los arranques en el grafo: la cuenta
+ perdura entre reinicios.
+
+## Fase 6.2 — E/S de disco asíncrona por interrupción (completada)
+
+La Fase 6.1 hizo hablar al disco, pero por **sondeo**: el procesador se quedaba
+en espera activa vigilando el *used ring* de virtio. La 6.2 libera el
+planificador cooperativo — la E/S de bloques pasa a ser **reactiva**:
+
+- `EsperaDisco` — una transferencia de bloques como `Future` nativo, sobre la
+ API no bloqueante de `virtio-drivers` (`read_blocks_nb` / `peek_used` /
+ `complete_*`). Cede la CPU mientras el disco trabaja.
+- La **IRQ del disco** — `montar` descubre la línea de IRQ legada del
+ dispositivo (registro «Interrupt Line» del espacio de configuración PCI), la
+ enruta por el 8259 y registra su manejador; `atender_irq` reconoce la
+ interrupción y despierta a la tarea que aguardaba el bloque.
+- `bloquear_en` — el puente para los contextos síncronos (el arranque, las
+ capacidades WASM): duerme la CPU con `hlt` en vez de sondear.
+
+Decisión de ingeniería: las IRQ se enrutan por el **PIC 8259** que el kernel ya
+gobierna, no por el IOAPIC — basta leer la línea que el firmware ya asignó.
+Verificado en QEMU: el disco se enruta a la IRQ 11; una tarea-sonda del reactor
+lee un bloque de forma asíncrona mientras las apps siguen pintando.
+
+Líneas abiertas posteriores: carga/descarga dinámica de apps desde el grafo de
+objetos; más capacidades del host (temporización, audio).
+
+## Principios que persisten entre fases
+
+- Reutilizar infraestructura madura de la comunidad antes que reinventar.
+- `unsafe` mínimo, confinado y justificado.
+- Verificar cada fase en QEMU antes de cerrarla.
+- `git commit` + `git push` tras cada iteración.
diff --git a/renaser/apps/cronista/Cargo.lock b/renaser/apps/cronista/Cargo.lock
new file mode 100644
index 0000000..d19e5f3
--- /dev/null
+++ b/renaser/apps/cronista/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "cronista"
+version = "0.1.0"
diff --git a/renaser/apps/cronista/Cargo.toml b/renaser/apps/cronista/Cargo.toml
new file mode 100644
index 0000000..46804b4
--- /dev/null
+++ b/renaser/apps/cronista/Cargo.toml
@@ -0,0 +1,31 @@
+# =============================================================================
+# renaser :: apps/cronista — Fase 6.1c :: el primer escriba del userspace
+# -----------------------------------------------------------------------------
+# Un modulo WebAssembly que, a diferencia de sus vecinas, deja HUELLA. En cada
+# arranque graba un objeto en el grafo persistente del kernel —enlazado al del
+# arranque anterior, formando una cadena— y lo corona como raiz. Asi lleva la
+# cronica de cuantas veces ha despertado el sistema: una cuenta que sobrevive
+# a los reinicios porque vive en el disco, no en la RAM.
+# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
+# =============================================================================
+
+[package]
+name = "cronista"
+version = "0.1.0"
+edition = "2021"
+description = "renaser :: app WASM cronista — la cronica persistente de los arranques"
+
+[workspace]
+
+# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
+# wasmi instancia.
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s"
+lto = true
diff --git a/renaser/apps/cronista/src/lib.rs b/renaser/apps/cronista/src/lib.rs
new file mode 100644
index 0000000..53d8ae7
--- /dev/null
+++ b/renaser/apps/cronista/src/lib.rs
@@ -0,0 +1,234 @@
+// =============================================================================
+// renaser :: apps/cronista — Fase 6.1c :: el primer escriba del userspace
+// -----------------------------------------------------------------------------
+// Las apps de fases anteriores eran efimeras: su mundo se borraba al apagar la
+// maquina. `cronista` es la primera que deja HUELLA. En cada arranque:
+//
+// 1. pregunta al kernel por la RAIZ del grafo de objetos —la cabeza de la
+// cadena de arranques anteriores—;
+// 2. lee de ella el numero del ultimo arranque;
+// 3. graba un objeto nuevo —sus datos: el numero de arranque; su hijo: la
+// raiz anterior— y lo corona como raiz;
+// 4. recorre la cadena entera para verificar que el DAG persiste integro;
+// 5. pinta una celda por cada arranque registrado.
+//
+// La cuenta NO vive en la RAM: vive en el disco, en el grafo direccionado por
+// contenido. Sobrevive a los reinicios. Cada vez que renaser despierta, la
+// cronista añade un eslabon a la cadena y una celda a su rejilla.
+//
+// Sus unicas vias hacia el mundo son las capacidades `sys_object_*` que el
+// kernel le inyecta. No conoce el disco, ni el bus PCI, ni el formato en
+// sectores: solo objetos, hashes y aristas.
+// =============================================================================
+
+#![no_std]
+
+// --- Las capacidades que el kernel `renaser` inyecta a esta aplicacion. ---
+#[link(wasm_import_module = "renaser")]
+extern "C" {
+ /// Compone un bufer de pixeles (de ESTA memoria lineal) en la region que el
+ /// kernel asigno a esta aplicacion.
+ fn sys_render_frame(ptr: u32, len: u32);
+
+ /// Graba un objeto en el grafo: `datos` es su carga util; `hijos` apunta a
+ /// un arreglo de `hijos_cnt` hashes de 32 bytes —las aristas—. El hash
+ /// resultante se escribe en `salida`. Devuelve 0 si todo fue bien.
+ fn sys_object_put(datos: u32, datos_len: u32, hijos: u32, hijos_cnt: u32, salida: u32) -> i32;
+
+ /// Copia la carga util del objeto `hash` en `salida`. Devuelve el numero de
+ /// bytes copiados, o un valor negativo si fallo.
+ fn sys_object_datos(hash: u32, salida: u32, capacidad: u32) -> i32;
+
+ /// Devuelve el numero de hijos del objeto `hash` y, si `indice` es valido,
+ /// escribe el hash de ese hijo en `salida`. Negativo si el objeto no existe.
+ fn sys_object_hijo(hash: u32, indice: u32, salida: u32) -> i32;
+
+ /// Escribe en `salida` el hash de la raiz del grafo. Devuelve 1 si hay
+ /// raiz, 0 si el grafo aun esta vacio.
+ fn sys_object_raiz(salida: u32) -> i32;
+
+ /// Corona el objeto `hash` como raiz del grafo. Devuelve 0 si lo logro.
+ fn sys_object_fijar_raiz(hash: u32) -> i32;
+}
+
+/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
+#[panic_handler]
+fn al_fallar(_: &core::panic::PanicInfo) -> ! {
+ loop {}
+}
+
+// --- Geometria de la escena. El ancho y el alto DEBEN coincidir con la region
+// que el kernel asigna a esta app. ---
+const ANCHO: usize = 360;
+const ALTO: usize = 80;
+
+/// Lado de paso de la rejilla de celdas, en pixeles.
+const PASO: usize = 20;
+/// Lado de una celda, en pixeles.
+const LADO: usize = 16;
+/// Celdas que caben en una fila.
+const POR_FILA: usize = ANCHO / PASO;
+/// Celdas que caben en la rejilla entera — el techo de lo que se pinta.
+const MAX_CELDAS: usize = POR_FILA * (ALTO / PASO);
+
+/// Indigo casi negro: el fondo del lienzo de la cronista.
+const FONDO: u32 = 0x0E_14_22;
+/// Ambar calido: una celda por arranque registrado.
+const CELDA: u32 = 0xF2_B2_33;
+/// Verde: el DAG se recorrio integro de la raiz al primer eslabon.
+const VERDE: u32 = 0x35_C4_6A;
+/// Rojo: la cadena se rompio — un objeto no resolvio.
+const ROJO: u32 = 0xD4_1E_2C;
+
+/// El lienzo de la aplicacion, en SU propia memoria lineal.
+static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
+
+// --- Buferes de intercambio con las capacidades `sys_object_*`. El kernel lee
+// y escribe hashes y datos AQUI, siempre dentro de esta memoria lineal. ---
+/// El hash de la raiz anterior — la cabeza de la cadena al arrancar.
+static mut HASH_RAIZ: [u8; 32] = [0; 32];
+/// El hash del objeto que esta cronista graba en este arranque.
+static mut HASH_NUEVO: [u8; 32] = [0; 32];
+/// Hash de trabajo para recorrer la cadena del DAG.
+static mut HASH_AUX: [u8; 32] = [0; 32];
+/// Los ocho bytes del numero de arranque, de ida y de vuelta del grafo.
+static mut DATOS_IO: [u8; 8] = [0; 8];
+
+/// Preparacion: el kernel la invoca UNA sola vez. Aqui ocurre toda la cronica —
+/// leer el grafo, grabar el eslabon nuevo, verificar el DAG y pintar.
+#[no_mangle]
+pub extern "C" fn init() {
+ // 1. ¿Hay ya una raiz? Es la cabeza de la cadena de arranques anteriores.
+ let tiene_raiz = unsafe { sys_object_raiz(core::ptr::addr_of_mut!(HASH_RAIZ) as u32) } == 1;
+
+ // 2. El numero del ultimo arranque vive en los `datos` de esa raiz.
+ let mut previo: u64 = 0;
+ if tiene_raiz {
+ let leidos = unsafe {
+ sys_object_datos(
+ core::ptr::addr_of!(HASH_RAIZ) as u32,
+ core::ptr::addr_of_mut!(DATOS_IO) as u32,
+ 8,
+ )
+ };
+ if leidos == 8 {
+ // SEGURIDAD: lectura de un escalar `Copy` de un estatico propio.
+ previo = u64::from_le_bytes(unsafe { *core::ptr::addr_of!(DATOS_IO) });
+ }
+ }
+ let cuenta = previo + 1;
+
+ // 3. Grabar el objeto de ESTE arranque. Sus `datos` son el numero de
+ // arranque; su unico hijo, la raiz anterior — el eslabon nuevo del DAG.
+ // SEGURIDAD: escritura de un escalar `Copy` a un estatico propio.
+ unsafe {
+ *core::ptr::addr_of_mut!(DATOS_IO) = cuenta.to_le_bytes();
+ }
+ let (hijos_ptr, hijos_cnt) = if tiene_raiz {
+ (core::ptr::addr_of!(HASH_RAIZ) as u32, 1u32)
+ } else {
+ (0u32, 0u32)
+ };
+ let grabado = unsafe {
+ sys_object_put(
+ core::ptr::addr_of!(DATOS_IO) as u32,
+ 8,
+ hijos_ptr,
+ hijos_cnt,
+ core::ptr::addr_of_mut!(HASH_NUEVO) as u32,
+ )
+ };
+
+ // 4. Coronar el objeto nuevo como raiz y verificar la integridad del DAG.
+ let mut integro = false;
+ if grabado == 0
+ && unsafe { sys_object_fijar_raiz(core::ptr::addr_of!(HASH_NUEVO) as u32) } == 0
+ {
+ integro = verificar_cadena(cuenta);
+ }
+
+ // 5. Pintar la cronica: una celda por arranque, un testigo de integridad.
+ pintar(cuenta, integro);
+}
+
+/// Un fotograma de trabajo. El numero de arranque no cambia durante una sesion:
+/// la cronica que `init` pinto persiste en el lienzo del kernel. `tick` solo
+/// cede el control, fiel al ABI cooperativo — no toda app necesita redibujar.
+#[no_mangle]
+pub extern "C" fn tick() {}
+
+/// Recorre la cadena del DAG desde el objeto recien grabado, descendiendo por
+/// el hijo 0, y comprueba que su profundidad coincide con el numero de
+/// arranque. Si coincide, el grafo entero se leyo de vuelta del disco integro.
+fn verificar_cadena(cuenta: u64) -> bool {
+ // Partir del objeto recien grabado.
+ // SEGURIDAD: copia de un arreglo `Copy` entre dos estaticos propios.
+ unsafe {
+ *core::ptr::addr_of_mut!(HASH_AUX) = *core::ptr::addr_of!(HASH_NUEVO);
+ }
+ let mut profundidad: u64 = 0;
+ loop {
+ profundidad += 1;
+ // `sys_object_hijo` lee el hash de HASH_AUX y, si hay hijo, escribe el
+ // del hijo 0 en el MISMO bufer: la cadena desciende un eslabon.
+ let hijos = unsafe {
+ sys_object_hijo(
+ core::ptr::addr_of!(HASH_AUX) as u32,
+ 0,
+ core::ptr::addr_of_mut!(HASH_AUX) as u32,
+ )
+ };
+ // Sin hijos: fin de la cadena — el primer arranque de todos. Un valor
+ // negativo seria un objeto que no resolvio: la cadena estaria rota.
+ if hijos <= 0 || profundidad >= 4096 {
+ break;
+ }
+ }
+ profundidad == cuenta
+}
+
+/// Pinta la cronica: el fondo, una celda ambar por arranque y, en la esquina,
+/// el testigo de integridad del grafo.
+fn pintar(cuenta: u64, integro: bool) {
+ // SEGURIDAD: durante `init` esta es la unica via de acceso a LIENZO, y el
+ // kernel jamas reentra el modulo mientras `init` corre.
+ let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
+ for pixel in lienzo.iter_mut() {
+ *pixel = FONDO;
+ }
+
+ // Una celda ambar por cada arranque registrado, dispuestas en rejilla.
+ let celdas = (cuenta as usize).min(MAX_CELDAS);
+ for i in 0..celdas {
+ let x = (i % POR_FILA) * PASO + 2;
+ let y = (i / POR_FILA) * PASO + 2;
+ rellenar(lienzo, x, y, LADO, LADO, CELDA);
+ }
+
+ // El testigo de integridad: verde si la cadena se recorrio entera de la
+ // raiz al primer eslabon, rojo si algo se rompio.
+ let testigo = if integro { VERDE } else { ROJO };
+ rellenar(lienzo, ANCHO - 14, 4, 10, 10, testigo);
+
+ // SEGURIDAD: `sys_render_frame` es una capacidad del host; el (ptr, len)
+ // describe nuestra propia memoria lineal y el host lo verifica sin piedad.
+ unsafe {
+ sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
+ }
+}
+
+/// Rellena un rectangulo, recortado con firmeza a los limites del lienzo.
+fn rellenar(lienzo: &mut [u32], x: usize, y: usize, ancho: usize, alto: usize, color: u32) {
+ let x1 = (x + ancho).min(ANCHO);
+ let y1 = (y + alto).min(ALTO);
+ let mut fila = y;
+ while fila < y1 {
+ let base = fila * ANCHO;
+ let mut col = x;
+ while col < x1 {
+ lienzo[base + col] = color;
+ col += 1;
+ }
+ fila += 1;
+ }
+}
diff --git a/renaser/apps/discola/Cargo.lock b/renaser/apps/discola/Cargo.lock
new file mode 100644
index 0000000..e06a61b
--- /dev/null
+++ b/renaser/apps/discola/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "discola"
+version = "0.1.0"
diff --git a/renaser/apps/discola/Cargo.toml b/renaser/apps/discola/Cargo.toml
new file mode 100644
index 0000000..c09c8b2
--- /dev/null
+++ b/renaser/apps/discola/Cargo.toml
@@ -0,0 +1,30 @@
+# =============================================================================
+# renaser :: apps/discola — el inquilino discolo del userspace
+# -----------------------------------------------------------------------------
+# Un modulo WebAssembly construido a proposito para portarse mal: su `tick`
+# cae en un bucle cerrado y jamas retorna. Existe para una sola cosa —
+# demostrar que el escudo de combustible (fuel) del kernel `renaser` lo
+# desaloja sin colgar el sistema ni perturbar a sus apps vecinas.
+# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
+# =============================================================================
+
+[package]
+name = "discola"
+version = "0.1.0"
+edition = "2021"
+description = "renaser :: app WASM discola — un bucle infinito que el fuel fulmina"
+
+[workspace]
+
+# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
+# wasmi instancia.
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s"
+lto = true
diff --git a/renaser/apps/discola/src/lib.rs b/renaser/apps/discola/src/lib.rs
new file mode 100644
index 0000000..f59e1cd
--- /dev/null
+++ b/renaser/apps/discola/src/lib.rs
@@ -0,0 +1,46 @@
+// =============================================================================
+// renaser :: apps/discola — Fase 5 :: el inquilino discolo del userspace
+// -----------------------------------------------------------------------------
+// Esta aplicacion esta MAL a proposito. Su `tick` no hace un fotograma de
+// trabajo y retorna —como manda el ABI cooperativo—: cae en un bucle cerrado
+// y jamas devuelve el control. En un sistema cooperativo ingenuo, eso colgaria
+// la maquina entera.
+//
+// Pero renaser ejecuta cada `tick` con un presupuesto estricto de COMBUSTIBLE.
+// Cuando este bucle lo agota, el runtime `wasmi` lanza una trampa, el kernel
+// recupera el mando y desaloja a este modulo — tatuando su region de purpura.
+// El resto del userspace ni se entera. Eso es lo que esta app demuestra.
+// =============================================================================
+
+#![no_std]
+
+/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
+#[panic_handler]
+fn al_fallar(_: &core::panic::PanicInfo) -> ! {
+ loop {}
+}
+
+/// Sumidero de las escrituras del bucle: obliga al compilador a CONSERVAR cada
+/// iteracion —y, con ella, su consumo de combustible— en lugar de elidirla.
+static mut SUMIDERO: u64 = 0;
+
+/// Preparacion. No hay nada honrado que preparar: el kernel la invoca, retorna
+/// sin incidentes, y la app pasa por buena... hasta su primer `tick`.
+#[no_mangle]
+pub extern "C" fn init() {}
+
+/// El fotograma que nunca termina. Un bucle cerrado, deliberado: jamas retorna.
+/// El kernel `renaser` lo cortara por agotamiento de combustible y desalojara
+/// esta aplicacion sin que el sistema sufra un solo sobresalto.
+#[no_mangle]
+pub extern "C" fn tick() {
+ let mut contador: u64 = 0;
+ loop {
+ contador = contador.wrapping_add(1);
+ // SEGURIDAD: escritura volatil a un escalar estatico; su unico fin es
+ // que el optimizador no pueda vaciar el bucle. No se crea referencia.
+ unsafe {
+ core::ptr::write_volatile(core::ptr::addr_of_mut!(SUMIDERO), contador);
+ }
+ }
+}
diff --git a/renaser/apps/glotona/Cargo.lock b/renaser/apps/glotona/Cargo.lock
new file mode 100644
index 0000000..de7981b
--- /dev/null
+++ b/renaser/apps/glotona/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "glotona"
+version = "0.1.0"
diff --git a/renaser/apps/glotona/Cargo.toml b/renaser/apps/glotona/Cargo.toml
new file mode 100644
index 0000000..d2cf8ae
--- /dev/null
+++ b/renaser/apps/glotona/Cargo.toml
@@ -0,0 +1,30 @@
+# =============================================================================
+# renaser :: apps/glotona — el inquilino voraz del userspace
+# -----------------------------------------------------------------------------
+# Un modulo WebAssembly construido a proposito para devorar memoria: su `tick`
+# reclama paginas de memoria lineal sin freno. Existe para demostrar que el
+# techo ESPACIAL del kernel `renaser` lo desaloja —baliza amarilla— sin agotar
+# el heap del sistema ni perturbar a sus apps vecinas.
+# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
+# =============================================================================
+
+[package]
+name = "glotona"
+version = "0.1.0"
+edition = "2021"
+description = "renaser :: app WASM glotona — devora memoria; el techo espacial la frena"
+
+[workspace]
+
+# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
+# wasmi instancia.
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s"
+lto = true
diff --git a/renaser/apps/glotona/src/lib.rs b/renaser/apps/glotona/src/lib.rs
new file mode 100644
index 0000000..d8b90e3
--- /dev/null
+++ b/renaser/apps/glotona/src/lib.rs
@@ -0,0 +1,39 @@
+// =============================================================================
+// renaser :: apps/glotona — Fase 6.0 :: el inquilino voraz del userspace
+// -----------------------------------------------------------------------------
+// Esta aplicacion esta MAL a proposito, como su hermana `discola` — pero en la
+// otra dimension. Donde `discola` devora TIEMPO (un bucle sin fin), `glotona`
+// devora ESPACIO: su `tick` invoca `memory.grow` reclamando memoria lineal sin
+// freno alguno.
+//
+// renaser le impone a cada modulo un techo de memoria. Cuando esta peticion lo
+// rebasa, el runtime `wasmi` —configurado para ello— lanza una trampa en vez
+// de devolver un discreto -1; el kernel la captura, desaloja a este modulo y
+// tiñe su region de amarillo palido. El heap del sistema jamas corre peligro.
+// =============================================================================
+
+#![no_std]
+
+/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
+#[panic_handler]
+fn al_fallar(_: &core::panic::PanicInfo) -> ! {
+ loop {}
+}
+
+/// Preparacion. Nada honrado que preparar: la app pasa por buena hasta su
+/// primer `tick`, igual que su hermana `discola`.
+#[no_mangle]
+pub extern "C" fn init() {}
+
+/// El fotograma voraz. Reclama 4096 paginas de memoria lineal de un golpe —
+/// 256 MiB, muy por encima de cualquier techo razonable. El kernel `renaser`
+/// denegara la expansion con una trampa y desalojara esta aplicacion.
+#[no_mangle]
+pub extern "C" fn tick() {
+ // `memory_grow` sobre la memoria 0. Con el techo espacial activo y la
+ // denegacion configurada como trampa, esta instruccion NO retorna: aborta.
+ let _ = core::arch::wasm32::memory_grow(0, 4096);
+ // Red de seguridad: si la denegacion no fuese una trampa, este bucle
+ // cerrado garantiza el desalojo —por combustible— de todos modos.
+ loop {}
+}
diff --git a/renaser/apps/hello_wasm/Cargo.lock b/renaser/apps/hello_wasm/Cargo.lock
new file mode 100644
index 0000000..a55539b
--- /dev/null
+++ b/renaser/apps/hello_wasm/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "hello_wasm"
+version = "0.1.0"
diff --git a/renaser/apps/hello_wasm/Cargo.toml b/renaser/apps/hello_wasm/Cargo.toml
new file mode 100644
index 0000000..716ef15
--- /dev/null
+++ b/renaser/apps/hello_wasm/Cargo.toml
@@ -0,0 +1,28 @@
+# =============================================================================
+# renaser :: apps/hello_wasm — primera aplicacion del userspace aislado
+# -----------------------------------------------------------------------------
+# No es un ELF: es un modulo WebAssembly puro. Se compila para wasm32 y el
+# kernel lo ejecuta dentro de wasmi, encerrado en su propia memoria lineal.
+# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
+# =============================================================================
+
+[package]
+name = "hello_wasm"
+version = "0.1.0"
+edition = "2021"
+description = "renaser :: app WASM de prueba — un cuadrado movil dirigido por teclado"
+
+[workspace]
+
+# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
+# wasmi instancia. La aplicacion solo habla con el kernel por funciones del host.
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s"
+lto = true
diff --git a/renaser/apps/hello_wasm/src/lib.rs b/renaser/apps/hello_wasm/src/lib.rs
new file mode 100644
index 0000000..90d661b
--- /dev/null
+++ b/renaser/apps/hello_wasm/src/lib.rs
@@ -0,0 +1,140 @@
+// =============================================================================
+// renaser :: apps/hello_wasm — Fase 4/5 :: el primer ciudadano del userspace
+// -----------------------------------------------------------------------------
+// Esta aplicacion vive DENTRO de su propia memoria lineal de WebAssembly. No
+// conoce la MMU, no conoce los anillos de privilegio de la CPU: su unica via
+// hacia el mundo son las dos capacidades que el kernel le inyecta. Lo que no
+// este importado, sencillamente, no tiene camino fisico que recorrer.
+//
+// FASE 5 :: el ABI deja de ser un `run()` que se queda dentro para siempre.
+// Ahora la app exporta `init()` —preparacion, una sola vez— y `tick()` —un
+// fotograma de trabajo, y RETORNA—. Ese retorno es el punto de cesion
+// cooperativa: el kernel recupera el control y atiende a las demas apps.
+// =============================================================================
+
+#![no_std]
+
+// --- Las dos UNICAS capacidades que el kernel `renaser` expone al modulo. ---
+#[link(wasm_import_module = "renaser")]
+extern "C" {
+ /// Compone un bufer de pixeles (de ESTA memoria lineal) en la region que el
+ /// kernel asigno a esta aplicacion.
+ fn sys_render_frame(ptr: u32, len: u32);
+ /// Devuelve el ultimo scancode crudo del teclado, o 0 si no hay ninguno.
+ fn sys_get_scancode() -> u32;
+}
+
+/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
+#[panic_handler]
+fn al_fallar(_: &core::panic::PanicInfo) -> ! {
+ loop {}
+}
+
+// --- Geometria de la escena. El ancho y el alto DEBEN coincidir con la region
+// que el kernel asigna a esta app: el host rechaza cualquier fotograma de
+// un tamaño que no sea, exactamente, el de su ventana. ---
+const ANCHO: usize = 480;
+const ALTO: usize = 560;
+const LADO: usize = 96;
+const PASO: i32 = 24;
+
+/// Azul nocturno: el fondo del lienzo de la aplicacion.
+const FONDO: u32 = 0x0A_18_30;
+/// Ambar: el cuadrado que el usuario gobierna.
+const CUADRO: u32 = 0xFF_B0_00;
+
+/// El lienzo de la aplicacion, en SU propia memoria lineal. El kernel jamas lo
+/// ve directamente: solo recibe el (ptr, len) que cada fotograma le entrega.
+static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
+
+/// Posicion del cuadrado. Vive entre fotogramas en la memoria lineal del modulo
+/// — el estado persiste porque la instancia, en la Fase 5, ya no es efimera.
+static mut POS_X: i32 = 0;
+static mut POS_Y: i32 = 0;
+
+/// Preparacion: el kernel la invoca UNA sola vez, al cargar el modulo. Pinta el
+/// fondo, centra el cuadrado y vuelca el primer fotograma.
+#[no_mangle]
+pub extern "C" fn init() {
+ // SEGURIDAD: durante `init` y `tick` esta es la unica via de acceso a
+ // LIENZO, y el kernel jamas reentra el modulo mientras una de ellas corre.
+ let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
+ for pixel in lienzo.iter_mut() {
+ *pixel = FONDO;
+ }
+
+ let x = (ANCHO / 2 - LADO / 2) as i32;
+ let y = (ALTO / 2 - LADO / 2) as i32;
+ rellenar(lienzo, x, y, CUADRO);
+ // SEGURIDAD: escritura de escalares `Copy`; no se crea referencia alguna.
+ unsafe {
+ POS_X = x;
+ POS_Y = y;
+ }
+
+ volcar(lienzo);
+}
+
+/// Un fotograma de trabajo: escucha el teclado, mueve el cuadrado, vuelca la
+/// imagen y RETORNA. El retorno cede la CPU al kernel y a las apps vecinas.
+#[no_mangle]
+pub extern "C" fn tick() {
+ let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
+ let (mut x, mut y) = unsafe { (POS_X, POS_Y) };
+
+ // 1. Escuchar al teclado a traves de la capacidad del host.
+ let (dx, dy) = match unsafe { sys_get_scancode() } {
+ 0x11 => (0, -PASO), // tecla W -> arriba
+ 0x1F => (0, PASO), // tecla S -> abajo
+ 0x1E => (-PASO, 0), // tecla A -> izquierda
+ 0x20 => (PASO, 0), // tecla D -> derecha
+ _ => (0, 0),
+ };
+
+ // 2. Borrar el cuadrado anterior repintando su hueco con el fondo.
+ rellenar(lienzo, x, y, FONDO);
+
+ // 3. Moverlo, manteniendolo siempre dentro del lienzo.
+ x = (x + dx).clamp(0, (ANCHO - LADO) as i32);
+ y = (y + dy).clamp(0, (ALTO - LADO) as i32);
+
+ // 4. Dibujar el cuadrado en su nueva posicion y guardar el estado.
+ rellenar(lienzo, x, y, CUADRO);
+ unsafe {
+ POS_X = x;
+ POS_Y = y;
+ }
+
+ // 5. Volcar el fotograma: el host lo compondra dentro de nuestra region.
+ volcar(lienzo);
+}
+
+/// Entrega el lienzo completo al kernel. El (ptr, len) apunta SIEMPRE dentro de
+/// nuestra memoria lineal, y su tamaño es, exactamente, el de la region.
+fn volcar(lienzo: &[u32]) {
+ // SEGURIDAD: `sys_render_frame` es una capacidad del host; el (ptr, len)
+ // describe nuestra propia memoria lineal y el host lo verifica sin piedad.
+ unsafe {
+ sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
+ }
+}
+
+/// Rellena un cuadrado de lado `LADO`, con su esquina en (x, y), recortado con
+/// firmeza a los limites del lienzo.
+fn rellenar(lienzo: &mut [u32], x: i32, y: i32, color: u32) {
+ let x0 = x.max(0) as usize;
+ let y0 = y.max(0) as usize;
+ let x1 = (x0 + LADO).min(ANCHO);
+ let y1 = (y0 + LADO).min(ALTO);
+
+ let mut fila = y0;
+ while fila < y1 {
+ let base = fila * ANCHO;
+ let mut col = x0;
+ while col < x1 {
+ lienzo[base + col] = color;
+ col += 1;
+ }
+ fila += 1;
+ }
+}
diff --git a/renaser/boot/Cargo.toml b/renaser/boot/Cargo.toml
new file mode 100644
index 0000000..b8f6485
--- /dev/null
+++ b/renaser/boot/Cargo.toml
@@ -0,0 +1,27 @@
+# =============================================================================
+# renaser :: boot (Fase 1.5) — orquestador host-side de empaquetado y arranque
+# -----------------------------------------------------------------------------
+# Este paquete se ejecuta en el ANFITRION (Artix Linux), nunca en bare-metal.
+# Toma el ELF nativo del kernel, lo fusiona con el cargador UEFI y lanza QEMU.
+# =============================================================================
+
+[package]
+name = "boot"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+description = "renaser :: constructor de imagen de disco UEFI y lanzador de QEMU"
+
+[dependencies]
+# Constructor de la imagen de disco UEFI. Corre en el anfitrion, usa `std`.
+bootloader.workspace = true
+
+# Dependencia de ARTEFACTO (RFC 3028). Cargo compila el kernel para
+# `x86_64-unknown-none` —en aislamiento total de arquitectura— y nos inyecta la
+# ruta de su ELF en la variable de entorno `CARGO_BIN_FILE_KERNEL_kernel`,
+# accesible desde `main.rs` mediante el macro `env!`.
+[dependencies.kernel]
+path = "../kernel"
+artifact = "bin"
+target = "x86_64-unknown-none"
diff --git a/renaser/boot/src/main.rs b/renaser/boot/src/main.rs
new file mode 100644
index 0000000..100a7c6
--- /dev/null
+++ b/renaser/boot/src/main.rs
@@ -0,0 +1,172 @@
+// =============================================================================
+// renaser :: boot/src/main.rs — Fase 1.5 :: el puente hacia el silicio
+// -----------------------------------------------------------------------------
+// Un kernel bare-metal no nace solo: alguien debe fusionarlo con un cargador,
+// sellarlo en una imagen de disco arrancable y entregarlo al hardware. Esa es
+// la unica mision de este orquestador de ANFITRION.
+//
+// El flujo es deliberadamente lineal y sin ambiguedad:
+//
+// 1. Localizar el ELF nativo del kernel (lo inyecta la dep. de artefacto).
+// 2. Fusionarlo con el cargador UEFI en una imagen de disco GPT.
+// 3. Lanzar QEMU con esa imagen y el firmware OVMF.
+//
+// Cada paso que pueda fallar lo hace en voz alta, con un mensaje accionable:
+// preferimos un error claro a un arranque silencioso hacia la nada.
+// =============================================================================
+
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+/// Ruta del ELF del kernel, ya compilado para `x86_64-unknown-none`.
+///
+/// La dependencia de artefacto define esta variable de entorno en tiempo de
+/// compilacion: cuando este binario de anfitrion existe, el kernel ya existe.
+const KERNEL_ELF: &str = env!("CARGO_BIN_FILE_KERNEL_kernel");
+
+/// Firmware UEFI OVMF tal como lo empaqueta Artix Linux (paquete `edk2-ovmf`).
+/// Es la imagen combinada codigo+variables, apta para `-bios`.
+const OVMF_POR_DEFECTO: &str = "/usr/share/edk2/x64/OVMF.4m.fd";
+
+/// Nombre de la imagen de disco UEFI que renaser genera.
+const NOMBRE_IMAGEN: &str = "renaser-uefi.img";
+
+/// Ruta del disco de objetos del grafo persistente (Fase 6.1c). Relativa al
+/// directorio de trabajo —la raiz del repo—, comun a `boot` y a QEMU.
+const NOMBRE_DISCO: &str = "target/disk.img";
+
+/// Tamaño del disco de objetos: 32 MiB. Se crea como fichero disperso.
+const TAM_DISCO: u64 = 32 * 1024 * 1024;
+
+fn main() {
+ if let Err(fallo) = orquestar() {
+ // Un error de orquestacion se anuncia en rojo y aborta con codigo 1:
+ // ninguna falla del anfitrion debe disfrazarse de exito.
+ eprintln!("\x1b[1;31m[renaser/boot] fallo:\x1b[0m {fallo}");
+ std::process::exit(1);
+ }
+}
+
+/// Ejecuta, en orden, las tres operaciones de la Fase 1.5.
+fn orquestar() -> Result<(), String> {
+ // --- 1. Localizar el artefacto del kernel. ---
+ let kernel = Path::new(KERNEL_ELF);
+ if !kernel.is_file() {
+ return Err(format!(
+ "no se encontro el ELF del kernel en {}\n \
+ (¿se interrumpio la compilacion de la dependencia de artefacto?)",
+ kernel.display()
+ ));
+ }
+ println!("[renaser/boot] kernel localizado :: {}", kernel.display());
+
+ // --- 2. Fusionar kernel + cargador UEFI en una imagen de disco. ---
+ let imagen = ruta_imagen(kernel);
+ println!("[renaser/boot] forjando imagen UEFI :: {}", imagen.display());
+ bootloader::UefiBoot::new(kernel)
+ .create_disk_image(&imagen)
+ .map_err(|e| format!("la crate `bootloader` no pudo crear la imagen UEFI: {e:?}"))?;
+
+ // --- 3. Garantizar el disco de objetos del grafo persistente. ---
+ preparar_disco_objetos()?;
+
+ // --- 4. Lanzar QEMU sobre esa imagen. ---
+ let ovmf = localizar_ovmf()?;
+ lanzar_qemu(&imagen, &ovmf)
+}
+
+/// Garantiza la existencia del disco de objetos del grafo persistente. Si no
+/// existe, lo forja como un fichero disperso de 32 MiB, ENTERAMENTE A CERO: el
+/// kernel, al no hallar la firma de su superbloque, lo formateara como un grafo
+/// virgen. Si ya existe, lo respeta — el grafo perdura entre arranques.
+fn preparar_disco_objetos() -> Result<(), String> {
+ let disco = Path::new(NOMBRE_DISCO);
+ if disco.is_file() {
+ println!("[renaser/boot] disco de objetos presente :: {}", disco.display());
+ return Ok(());
+ }
+ if let Some(directorio) = disco.parent() {
+ std::fs::create_dir_all(directorio)
+ .map_err(|e| format!("no se pudo crear el directorio del disco de objetos: {e}"))?;
+ }
+ // Forjar el disco: un fichero disperso, a cero, de 32 MiB. El kernel
+ // escribira su superbloque la primera vez que lo monte.
+ let fichero = std::fs::File::create(disco)
+ .map_err(|e| format!("no se pudo crear el disco de objetos «{}»: {e}", disco.display()))?;
+ fichero
+ .set_len(TAM_DISCO)
+ .map_err(|e| format!("no se pudo dimensionar el disco de objetos: {e}"))?;
+ println!(
+ "[renaser/boot] disco de objetos forjado :: {} ({} MiB, virgen)",
+ disco.display(),
+ TAM_DISCO / (1024 * 1024)
+ );
+ Ok(())
+}
+
+/// Calcula la ruta de la imagen: junto al propio ELF del kernel, es decir,
+/// dentro de `target/`. Una ubicacion predecible y siempre escribible.
+fn ruta_imagen(kernel: &Path) -> PathBuf {
+ kernel
+ .parent()
+ .unwrap_or_else(|| Path::new("."))
+ .join(NOMBRE_IMAGEN)
+}
+
+/// Resuelve la ruta del firmware OVMF. Permite sobreescribirla con la variable
+/// de entorno `RENASER_OVMF` para entornos cuyo `edk2-ovmf` viva en otra ruta.
+fn localizar_ovmf() -> Result<String, String> {
+ let ruta = std::env::var("RENASER_OVMF").unwrap_or_else(|_| OVMF_POR_DEFECTO.to_string());
+ if Path::new(&ruta).is_file() {
+ Ok(ruta)
+ } else {
+ Err(format!(
+ "firmware UEFI OVMF no encontrado en «{ruta}»\n \
+ instala el paquete `edk2-ovmf`, o exporta RENASER_OVMF=<ruta a OVMF.fd>"
+ ))
+ }
+}
+
+/// Invoca QEMU como subproceso. Los argumentos se ciñen a las primitivas
+/// minimas necesarias para que el Framebuffer GOP cobre vida:
+///
+/// * `-bios` firmware UEFI OVMF.
+/// * `-drive raw` la imagen de disco UEFI, sin capa de traduccion.
+/// * `-vga std` VGA estandar => framebuffer lineal que el GOP expone.
+/// * `-serial stdio` telemetria serial del procesador hacia esta consola.
+/// * `--no-reboot` un fallo triple detiene la maquina en vez de reiniciar
+/// en bucle: asi la baliza de panico permanece visible.
+/// * `virtio-blk-pci` el disco de objetos, sobre el bus PCI (q35 es x86_64;
+/// `virtio-blk-device`, su gemelo MMIO, es cosa de ARM).
+fn lanzar_qemu(imagen: &Path, ovmf: &str) -> Result<(), String> {
+ println!("[renaser/boot] arrancando QEMU :: la superficie indigo nace ahora\n");
+
+ let mut qemu = Command::new("qemu-system-x86_64");
+ // `accel=kvm:tcg` intenta KVM y, si no esta disponible, recae en TCG puro.
+ qemu.arg("-machine").arg("q35,accel=kvm:tcg")
+ .arg("-m").arg("256M")
+ .arg("-bios").arg(ovmf)
+ .arg("-drive").arg(format!("format=raw,file={}", imagen.display()))
+ .arg("-vga").arg("std")
+ .arg("-serial").arg("stdio")
+ .arg("--no-reboot")
+ // El disco de objetos, como dispositivo virtio-blk sobre el bus PCI.
+ .arg("-drive").arg(format!("format=raw,file={NOMBRE_DISCO},if=none,id=drv0"))
+ .arg("-device").arg("virtio-blk-pci,drive=drv0");
+
+ // Cualquier argumento extra tras `--` se reenvia a QEMU intacto.
+ // Ejemplo: `cargo run -p boot -- -display none -d int`.
+ qemu.args(std::env::args().skip(1));
+
+ match qemu.status() {
+ Ok(estado) if estado.success() => {
+ println!("\n[renaser/boot] QEMU finalizo limpiamente.");
+ Ok(())
+ }
+ Ok(estado) => Err(format!("QEMU termino con estado anomalo: {estado}")),
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(
+ "`qemu-system-x86_64` no esta en el PATH; instala el paquete `qemu-full`".to_string(),
+ ),
+ Err(e) => Err(format!("no se pudo ejecutar QEMU: {e}")),
+ }
+}
diff --git a/renaser/kernel/Cargo.toml b/renaser/kernel/Cargo.toml
new file mode 100644
index 0000000..3ea7bda
--- /dev/null
+++ b/renaser/kernel/Cargo.toml
@@ -0,0 +1,68 @@
+# =============================================================================
+# renaser :: kernel — el corazon bare-metal que late en el espacio unico
+# -----------------------------------------------------------------------------
+# Este paquete esta EXCLUIDO del espacio de trabajo (ver el Cargo.toml raiz):
+# es codigo puramente bare-metal y solo se compila como dependencia de
+# artefacto de `boot`, que le impone el target `x86_64-unknown-none`. Por eso
+# fija sus versiones de forma explicita, sin herencia del workspace.
+# =============================================================================
+
+[package]
+name = "kernel"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+authors = ["JL Soltech <gerencia@jlsoltech.com>"]
+description = "renaser :: kernel asincrono SASOS — entrada, framebuffer y reactor"
+
+# El kernel es un binario freestanding: sin arnes de pruebas ni de benchmarks.
+[[bin]]
+name = "kernel"
+path = "src/main.rs"
+test = false
+bench = false
+doctest = false
+
+[dependencies]
+# --- Fase 1-2 :: arranque, framebuffer e interrupciones ---
+bootloader_api = "0.11"
+x86_64 = "0.15"
+embedded-graphics = "0.8"
+
+# --- Fase 3 :: heap dinamico, reactor asincrono y texto vectorial ---
+linked_list_allocator = "0.10"
+spin = "0.9"
+crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] }
+futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
+# `hashbrown` ACTIVA el modo `no_std` de fontdue; sin el recae en `std`.
+fontdue = { version = "0.9", default-features = false, features = ["hashbrown"] }
+
+# --- Fase 4 :: interprete WebAssembly del userspace aislado ---
+# `wasmi` bare-metal: sin `std`, sin `wat`, sin `simd` (el target no tiene SSE).
+wasmi = { version = "1.0", default-features = false, features = ["hash-collections"] }
+
+# --- Fase 6 :: drivers de hardware — el disco virtio-blk sobre el bus PCI ---
+# `virtio-drivers` bare-metal: el kernel implementa su `trait Hal` para el DMA.
+virtio-drivers = { version = "0.13", default-features = false, features = ["alloc"] }
+
+# --- Fase 6.1c :: el grafo de objetos direccionado por contenido ---
+# `serde` da el rasgo de (de)serializacion; `postcard` lo materializa en un
+# formato binario compacto, pensado para sistemas empotrados — el que viaja al
+# disco. Ambos `no_std`, apoyados en `alloc`.
+serde = { version = "1", default-features = false, features = ["alloc", "derive"] }
+postcard = { version = "1", default-features = false, features = ["alloc"] }
+# `blake3`: la funcion hash que da identidad a cada objeto. Se fuerza la
+# implementacion ESCALAR pura (`pure` + los cuatro `no_*`): el target del kernel
+# corre sin SSE, y un camino SIMD activado por deteccion en tiempo de ejecucion
+# ejecutaria instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaria con un #UD.
+blake3 = { version = "1", default-features = false, features = [
+ "pure", "no_sse2", "no_sse41", "no_avx2", "no_avx512",
+] }
+
+# --- Fase 8 (preparación) :: el compositor ---
+# `mirada-layout` es el motor de teselado del compositor de brahman —
+# geometría pura (rectángulos, foco, Z-order), `no_std`, sin smithay ni
+# Wayland. Vive en el monorepo brahman, en su PROPIO workspace; renaser
+# lo enlaza por `path` cruzando la frontera. La feature `serde` queda
+# APAGADA: el kernel computa el layout en memoria, no lo serializa.
+mirada-layout = { path = "../../crates/modules/mirada/mirada-layout" }
diff --git a/renaser/kernel/assets/app.wasm b/renaser/kernel/assets/app.wasm
new file mode 100755
index 0000000..3660612
Binary files /dev/null and b/renaser/kernel/assets/app.wasm differ
diff --git a/renaser/kernel/assets/cronista.wasm b/renaser/kernel/assets/cronista.wasm
new file mode 100755
index 0000000..3b06a01
Binary files /dev/null and b/renaser/kernel/assets/cronista.wasm differ
diff --git a/renaser/kernel/assets/discola.wasm b/renaser/kernel/assets/discola.wasm
new file mode 100755
index 0000000..f416e59
Binary files /dev/null and b/renaser/kernel/assets/discola.wasm differ
diff --git a/renaser/kernel/assets/font.ttf b/renaser/kernel/assets/font.ttf
new file mode 100644
index 0000000..7f95a67
Binary files /dev/null and b/renaser/kernel/assets/font.ttf differ
diff --git a/renaser/kernel/assets/glotona.wasm b/renaser/kernel/assets/glotona.wasm
new file mode 100755
index 0000000..aade2fc
Binary files /dev/null and b/renaser/kernel/assets/glotona.wasm differ
diff --git a/renaser/kernel/src/almacen.rs b/renaser/kernel/src/almacen.rs
new file mode 100644
index 0000000..025ff00
--- /dev/null
+++ b/renaser/kernel/src/almacen.rs
@@ -0,0 +1,292 @@
+// =============================================================================
+// renaser :: kernel/src/almacen.rs — Fase 6.1c :: el grafo de objetos
+// -----------------------------------------------------------------------------
+// renaser rompe con POSIX tambien en el almacenamiento: aqui no hay un sistema
+// de archivos plano —rutas, directorios, inodos—. Hay un GRAFO DIRIGIDO ACICLICO
+// de objetos DIRECCIONADOS POR CONTENIDO.
+//
+// Un objeto es una carga util de bytes y una lista de aristas hacia otros
+// objetos. Su IDENTIDAD no es un nombre ni un numero: es el hash BLAKE3 de su
+// forma serializada. De ello se siguen dos propiedades que un FS jamas regala:
+//
+// * INTEGRIDAD — el hash verifica el contenido; un objeto corrupto se delata.
+// * DEDUPLICACION — contenido identico produce hash identico; se almacena
+// una sola vez, aunque mil aristas apunten a el.
+//
+// El disco se organiza como un LOG: el sector 0 es el superbloque —el ancla
+// del grafo—, y tras el se anexan los registros de objetos, uno tras otro. Un
+// indice en memoria (hash -> sector) se reconstruye al arrancar recorriendo el
+// log. La serializacion la hace `postcard`: binaria, compacta, determinista.
+// =============================================================================
+
+use alloc::collections::BTreeMap;
+use alloc::vec;
+use alloc::vec::Vec;
+
+use serde::{Deserialize, Serialize};
+use spin::{Mutex, Once};
+
+use crate::drivers::disco::{self, TAM_SECTOR};
+
+/// Firma magica del superbloque — «RENASer GRaFo». Distingue un disco de
+/// renaser de uno virgen o ajeno.
+const MAGIA: [u8; 8] = *b"RENASGRF";
+
+/// Version del formato en disco. Un disco con otra version se reformatea.
+const VERSION: u32 = 1;
+
+/// Techo del tamaño de un objeto serializado: 1 MiB. Acota los buferes de E/S
+/// y permite descartar un registro corrupto sin intentar leer un disparate.
+const MAX_OBJETO: usize = 1024 * 1024;
+
+/// El identificador de un objeto: el hash BLAKE3 de su forma serializada. En un
+/// almacen direccionado por contenido, la identidad ES el contenido.
+pub type Hash = [u8; 32];
+
+/// Un objeto del grafo: una carga util opaca y las aristas que lo enlazan con
+/// otros objetos. Los `hijos` hacen del almacen un DAG —no un arbol, no una
+/// lista—: un objeto puede ser hijo de muchos, y el direccionamiento por
+/// contenido garantiza que cada contenido distinto se guarda una sola vez.
+#[derive(Serialize, Deserialize, Clone)]
+pub struct Objeto {
+ /// La carga util del objeto: bytes crudos, que el kernel no interpreta.
+ pub datos: Vec<u8>,
+ /// Los hashes de los objetos hijos: las aristas salientes del DAG.
+ pub hijos: Vec<Hash>,
+}
+
+/// El superbloque: el sector 0 del disco. Ancla el grafo entero — dice por
+/// donde continua el log y cual es el objeto raiz.
+#[derive(Serialize, Deserialize)]
+struct SuperBloque {
+ /// Firma magica: debe ser [`MAGIA`].
+ magia: [u8; 8],
+ /// Version del formato: debe ser [`VERSION`].
+ version: u32,
+ /// Proximo sector libre del log — donde se anexara el siguiente objeto.
+ cursor: u64,
+ /// El objeto raiz del DAG: el punto de entrada que el userspace fija y lee.
+ raiz: Option<Hash>,
+}
+
+/// El estado vivo del almacen: el cursor del log, la raiz y el indice en
+/// memoria que traduce cada hash al sector donde habita su registro.
+struct Almacen {
+ /// Proximo sector libre del log.
+ cursor: u64,
+ /// El objeto raiz del DAG.
+ raiz: Option<Hash>,
+ /// Indice hash -> sector del registro. Se reconstruye al arrancar.
+ indice: BTreeMap<Hash, u64>,
+ /// Capacidad del disco, en sectores.
+ capacidad: u64,
+}
+
+/// El almacen global de renaser. Se funde una sola vez, en `init`.
+static ALMACEN: Once<Mutex<Almacen>> = Once::new();
+
+/// El fruto de fundar el almacen — para que el arranque deje constancia visual.
+pub struct Resumen {
+ /// Capacidad del disco, en sectores.
+ pub capacidad: u64,
+ /// Numero de objetos hallados en el grafo.
+ pub objetos: usize,
+ /// ¿Tiene el grafo un objeto raiz?
+ pub raiz: bool,
+ /// ¿Se reformateo el disco (estaba virgen o era ajeno)?
+ pub formateado: bool,
+}
+
+/// Numero de sectores que ocupa un registro cuyo payload mide `longitud` bytes.
+/// Cada registro en disco es: `[longitud: u32 LE][payload postcard][relleno 0]`.
+fn sectores_registro(longitud: usize) -> u64 {
+ (4 + longitud).div_ceil(TAM_SECTOR) as u64
+}
+
+/// Funda el almacen de objetos: monta el disco, lee el superbloque y, si el
+/// disco ya es de renaser, reconstruye el indice recorriendo el log; si es
+/// virgen o ajeno, lo formatea. Toda falla se devuelve como `Err`.
+pub fn init() -> Result<Resumen, &'static str> {
+ let capacidad = disco::montar()?;
+ if capacidad < 2 {
+ return Err("el disco es demasiado pequeño para un grafo");
+ }
+
+ // Leer el sector 0 e intentar interpretarlo como superbloque de renaser.
+ let mut sector0 = [0u8; TAM_SECTOR];
+ disco::leer_sectores(0, &mut sector0)?;
+
+ let (cursor, raiz, indice, formateado) =
+ match postcard::take_from_bytes::<SuperBloque>(§or0) {
+ // Disco de renaser, con la version corriente: adoptar su grafo.
+ Ok((sb, _)) if sb.magia == MAGIA && sb.version == VERSION => {
+ let indice = reconstruir_indice(sb.cursor)?;
+ (sb.cursor, sb.raiz, indice, false)
+ }
+ // Disco virgen, ajeno o de otra version: empezar de cero. El log
+ // arranca en el sector 1, justo despues del superbloque.
+ _ => (1, None, BTreeMap::new(), true),
+ };
+
+ let objetos = indice.len();
+ let tiene_raiz = raiz.is_some();
+ let almacen = Almacen {
+ cursor,
+ raiz,
+ indice,
+ capacidad,
+ };
+
+ // Un disco recien formateado necesita su superbloque grabado de inmediato.
+ if formateado {
+ persistir(&almacen)?;
+ }
+ ALMACEN.call_once(|| Mutex::new(almacen));
+
+ Ok(Resumen {
+ capacidad,
+ objetos,
+ raiz: tiene_raiz,
+ formateado,
+ })
+}
+
+/// Recorre el log —del sector 1 al `cursor`— y reconstruye el indice
+/// hash -> sector. Cada registro se rehashea: el indice se reconstruye, no se
+/// confia. Un registro corrupto detiene el escaneo sin incendiar nada.
+fn reconstruir_indice(cursor: u64) -> Result<BTreeMap<Hash, u64>, &'static str> {
+ let mut indice = BTreeMap::new();
+ let mut sector: u64 = 1;
+ while sector < cursor {
+ let payload = leer_registro(sector)?;
+ match payload {
+ // Un payload valido: hashearlo e indexarlo.
+ Some(payload) => {
+ let n = sectores_registro(payload.len());
+ let hash = *blake3::hash(&payload).as_bytes();
+ indice.insert(hash, sector);
+ sector += n;
+ }
+ // Cabecera a cero o longitud imposible: fin (o corrupcion) del log.
+ None => break,
+ }
+ }
+ Ok(indice)
+}
+
+/// Lee el registro que arranca en `sector` y devuelve su payload postcard
+/// (sin la cabecera de longitud ni el relleno). `None` si la cabecera dice
+/// longitud cero —fin del log— o una longitud imposible —corrupcion—.
+fn leer_registro(sector: u64) -> Result<Option<Vec<u8>>, &'static str> {
+ let mut cabecera = [0u8; TAM_SECTOR];
+ disco::leer_sectores(sector, &mut cabecera)?;
+ let longitud =
+ u32::from_le_bytes([cabecera[0], cabecera[1], cabecera[2], cabecera[3]]) as usize;
+ if longitud == 0 || longitud > MAX_OBJETO {
+ return Ok(None);
+ }
+ let n = sectores_registro(longitud) as usize;
+ // Si el registro cabe en el sector ya leido, evitar una segunda lectura.
+ let payload = if n == 1 {
+ cabecera[4..4 + longitud].to_vec()
+ } else {
+ let mut buf = vec![0u8; n * TAM_SECTOR];
+ disco::leer_sectores(sector, &mut buf)?;
+ buf[4..4 + longitud].to_vec()
+ };
+ Ok(Some(payload))
+}
+
+/// Graba el superbloque —el ancla del grafo— en el sector 0.
+fn persistir(almacen: &Almacen) -> Result<(), &'static str> {
+ let sb = SuperBloque {
+ magia: MAGIA,
+ version: VERSION,
+ cursor: almacen.cursor,
+ raiz: almacen.raiz,
+ };
+ let bytes = postcard::to_allocvec(&sb).map_err(|_| "no se pudo serializar el superbloque")?;
+ if bytes.len() > TAM_SECTOR {
+ return Err("el superbloque no cabe en un sector");
+ }
+ let mut sector0 = [0u8; TAM_SECTOR];
+ sector0[..bytes.len()].copy_from_slice(&bytes);
+ disco::escribir_sectores(0, §or0)
+}
+
+/// Almacena un objeto y devuelve su hash. Direccionamiento por contenido en
+/// estado puro: si un objeto de contenido identico ya existe, NO se reescribe —
+/// se devuelve el hash que ya tenia. El grafo nunca guarda dos veces lo mismo.
+pub fn almacenar(datos: Vec<u8>, hijos: Vec<Hash>) -> Result<Hash, &'static str> {
+ let objeto = Objeto { datos, hijos };
+ let bytes =
+ postcard::to_allocvec(&objeto).map_err(|_| "no se pudo serializar el objeto")?;
+ if bytes.is_empty() || bytes.len() > MAX_OBJETO {
+ return Err("el objeto tiene un tamaño invalido");
+ }
+ // La identidad del objeto: el hash de su forma serializada.
+ let hash = *blake3::hash(&bytes).as_bytes();
+
+ let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
+ let mut almacen = mutex.lock();
+
+ // ¿Ya esta en el grafo? Entonces no hay nada que grabar.
+ if almacen.indice.contains_key(&hash) {
+ return Ok(hash);
+ }
+
+ // Reservar los sectores del registro al final del log.
+ let n = sectores_registro(bytes.len());
+ if almacen.cursor + n > almacen.capacidad {
+ return Err("el grafo de objetos esta lleno");
+ }
+ let sector = almacen.cursor;
+
+ // Componer el registro: [longitud][payload][relleno a cero] y grabarlo.
+ let mut registro = vec![0u8; n as usize * TAM_SECTOR];
+ registro[0..4].copy_from_slice(&(bytes.len() as u32).to_le_bytes());
+ registro[4..4 + bytes.len()].copy_from_slice(&bytes);
+ disco::escribir_sectores(sector, ®istro)?;
+
+ // El objeto ya esta en disco: avanzar el cursor, indexarlo y RE-anclar el
+ // superbloque. El orden importa — el superbloque se graba el ultimo, de
+ // modo que jamas apunte a un registro a medio escribir.
+ almacen.cursor += n;
+ almacen.indice.insert(hash, sector);
+ persistir(&almacen)?;
+
+ Ok(hash)
+}
+
+/// Recupera un objeto por su hash. `Ok(None)` si el hash no esta en el grafo.
+pub fn recuperar(hash: &Hash) -> Result<Option<Objeto>, &'static str> {
+ let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
+ // Soltar el cerrojo del almacen ANTES de la E/S de disco —lenta, por
+ // sondeo—: el indice ya entrego el sector, y nada mas reclama el cerrojo.
+ let sector = match mutex.lock().indice.get(hash) {
+ Some(&s) => s,
+ None => return Ok(None),
+ };
+ let payload = leer_registro(sector)?.ok_or("registro de objeto corrupto")?;
+ // Verificacion de integridad: el contenido leido DEBE rehashear al hash
+ // pedido. Si no, el disco ha mentido — y se delata.
+ if *blake3::hash(&payload).as_bytes() != *hash {
+ return Err("el objeto no supero la verificacion de integridad");
+ }
+ let (objeto, _) = postcard::take_from_bytes::<Objeto>(&payload)
+ .map_err(|_| "no se pudo deserializar el objeto")?;
+ Ok(Some(objeto))
+}
+
+/// El hash del objeto raiz del grafo, si lo hay.
+pub fn raiz() -> Option<Hash> {
+ ALMACEN.get().and_then(|mutex| mutex.lock().raiz)
+}
+
+/// Corona un objeto como raiz del grafo y ancla el cambio en el superbloque.
+pub fn fijar_raiz(hash: Hash) -> Result<(), &'static str> {
+ let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
+ let mut almacen = mutex.lock();
+ almacen.raiz = Some(hash);
+ persistir(&almacen)
+}
diff --git a/renaser/kernel/src/async_system/executor.rs b/renaser/kernel/src/async_system/executor.rs
new file mode 100644
index 0000000..f6c3dbb
--- /dev/null
+++ b/renaser/kernel/src/async_system/executor.rs
@@ -0,0 +1,103 @@
+// =============================================================================
+// renaser :: async_system/executor.rs — Fase 3 :: el reactor cooperativo
+// -----------------------------------------------------------------------------
+// El ejecutor es el corazon reactivo de renaser. Mantiene el censo de tareas
+// vivas y una cola de las que estan listas para avanzar. Cuando no queda nada
+// por hacer, no malgasta la CPU en un bucle ocupado: la duerme con `hlt`
+// hasta que el proximo impulso de hardware la despierte.
+// =============================================================================
+
+use alloc::collections::{BTreeMap, VecDeque};
+use alloc::sync::Arc;
+use core::future::Future;
+use core::task::{Context, Poll, Waker};
+
+use spin::Mutex;
+use x86_64::instructions::interrupts;
+
+use super::task::{Task, TaskId};
+use super::waker::{self, ColaListas};
+
+/// El ejecutor cooperativo de renaser.
+pub struct Executor {
+ /// Censo de todas las tareas vivas, indexadas por su identidad.
+ tareas: BTreeMap<TaskId, Task>,
+ /// Cola de tareas listas para su proximo avance.
+ cola_listas: ColaListas,
+ /// Cache de wakers: uno por tarea, reutilizado entre avances.
+ cache_wakers: BTreeMap<TaskId, Waker>,
+}
+
+impl Executor {
+ /// Crea un ejecutor vacio. Requiere que el heap ya este fundado.
+ pub fn nuevo() -> Executor {
+ Executor {
+ tareas: BTreeMap::new(),
+ cola_listas: Arc::new(Mutex::new(VecDeque::new())),
+ cache_wakers: BTreeMap::new(),
+ }
+ }
+
+ /// Da de alta una tarea nueva y la marca como lista para su primer avance.
+ pub fn spawn(&mut self, futuro: impl Future<Output = ()> + Send + 'static) {
+ let tarea = Task::nueva(futuro);
+ let id = tarea.id;
+ if self.tareas.insert(id, tarea).is_some() {
+ panic!("renaser :: TaskId duplicado — lo imposible ha ocurrido");
+ }
+ interrupts::without_interrupts(|| self.cola_listas.lock().push_back(id));
+ }
+
+ /// Avanza, hasta agotarla, la cola de tareas listas.
+ fn avanzar_listas(&mut self) {
+ loop {
+ // Sacar el siguiente id con las interrupciones acalladas: la cola
+ // es compartida con los wakers que se disparan desde las IRQ.
+ let siguiente = interrupts::without_interrupts(|| self.cola_listas.lock().pop_front());
+ let id = match siguiente {
+ Some(id) => id,
+ None => return,
+ };
+ let tarea = match self.tareas.get_mut(&id) {
+ Some(t) => t,
+ None => continue, // la tarea concluyo en una vuelta previa
+ };
+ // Un waker por tarea, reutilizado: reinyecta esta tarea en la cola.
+ let cola = self.cola_listas.clone();
+ let despertador = self
+ .cache_wakers
+ .entry(id)
+ .or_insert_with(|| waker::crear(id, cola));
+ let mut contexto = Context::from_waker(despertador);
+ if let Poll::Ready(()) = tarea.poll(&mut contexto) {
+ // La tarea termino: se retira del censo y se libera su waker.
+ self.tareas.remove(&id);
+ self.cache_wakers.remove(&id);
+ }
+ }
+ }
+
+ /// Si no queda trabajo, duerme la CPU hasta la proxima interrupcion. El
+ /// chequeo y el `hlt` se hacen con las interrupciones acalladas para que
+ /// ningun despertar se pierda en la rendija entre uno y otro.
+ fn dormir_si_inactivo(&self) {
+ interrupts::disable();
+ let hay_trabajo = !self.cola_listas.lock().is_empty();
+ if hay_trabajo {
+ // Llego trabajo justo ahora: reactivar y seguir sin dormir.
+ interrupts::enable();
+ } else {
+ // `sti; hlt` atomico: habilita y duerme sin condicion de carrera.
+ interrupts::enable_and_hlt();
+ }
+ }
+
+ /// Cede el hilo principal al reactor. No retorna jamas: desde aqui, renaser
+ /// vive del latir de sus interrupciones.
+ pub fn run(&mut self) -> ! {
+ loop {
+ self.avanzar_listas();
+ self.dormir_si_inactivo();
+ }
+ }
+}
diff --git a/renaser/kernel/src/async_system/mod.rs b/renaser/kernel/src/async_system/mod.rs
new file mode 100644
index 0000000..fcf0569
--- /dev/null
+++ b/renaser/kernel/src/async_system/mod.rs
@@ -0,0 +1,16 @@
+// =============================================================================
+// renaser :: kernel/src/async_system — el reactor cooperativo (Fase 3 / 5)
+// -----------------------------------------------------------------------------
+// Aqui renaser rompe con el modelo de hilos pesados de Linux. No hay cambio
+// de contexto en la CPU: las interrupciones de hardware no conmutan pilas,
+// DESPIERTAN tareas. El kernel avanza cooperativamente, una tarea cede y la
+// siguiente toma el relevo. Sobre estos cimientos corre, desde la Fase 5, el
+// bytecode WASM aislado por software de la Fase 4: cada aplicacion es una
+// tarea mas, y el `reloj` le marca el compas de sus fotogramas.
+// =============================================================================
+
+pub mod executor;
+pub mod reloj;
+pub mod task;
+pub mod teclado;
+pub mod waker;
diff --git a/renaser/kernel/src/async_system/reloj.rs b/renaser/kernel/src/async_system/reloj.rs
new file mode 100644
index 0000000..00320ab
--- /dev/null
+++ b/renaser/kernel/src/async_system/reloj.rs
@@ -0,0 +1,101 @@
+// =============================================================================
+// renaser :: async_system/reloj.rs — Fase 5 :: el compas de los fotogramas
+// -----------------------------------------------------------------------------
+// El temporizador (PIT, IRQ0) ya no solo despierta al ejecutor de su `hlt`:
+// ahora marca el COMPAS del userspace. Cada pulso a 100 Hz es un fotograma —
+// una oportunidad de cesion cooperativa. `EsperaFrame` convierte ese pulso de
+// hardware en un `Future`: una tarea WASM hace su trabajo de un fotograma y
+// `.await`-ea el siguiente, cediendo la CPU a sus vecinas mientras tanto.
+// =============================================================================
+
+use alloc::vec::Vec;
+use core::future::Future;
+use core::pin::Pin;
+use core::sync::atomic::{AtomicU64, Ordering};
+use core::task::{Context, Poll, Waker};
+
+use spin::{Mutex, Once};
+use x86_64::instructions::interrupts;
+
+/// Pulsos del temporizador acumulados desde el arranque. Lo incrementa la IRQ0;
+/// lo consulta `EsperaFrame`. Atomico: la IRQ y el hilo principal lo comparten.
+static CONTADOR_PULSOS: AtomicU64 = AtomicU64::new(0);
+
+/// Censo de wakers en espera del proximo fotograma. Vive en el heap —de ahi el
+/// `Once`—; tras su `Mutex`, lo disputan el lado tarea y el manejador de IRQ0.
+static EN_ESPERA: Once<Mutex<Vec<Waker>>> = Once::new();
+
+/// Funda el censo de wakers del reloj. Requiere el heap ya activo; debe
+/// invocarse una sola vez, antes de habilitar las interrupciones.
+pub fn init() {
+ EN_ESPERA.call_once(|| Mutex::new(Vec::new()));
+}
+
+/// Punto de entrada DESDE el manejador de IRQ0. Avanza el contador de pulsos y
+/// despierta a cuantas tareas aguardaban el fotograma. Deliberadamente breve y
+/// libre de panicos: corre en contexto de interrupcion.
+pub fn pulso() {
+ // `Release`: el avance del contador se publica ANTES de que los wakers
+ // reinyecten sus tareas; el `poll` que sigue lo leera con `Acquire` y vera,
+ // garantizado, el valor nuevo.
+ CONTADOR_PULSOS.fetch_add(1, Ordering::Release);
+ if let Some(censo) = EN_ESPERA.get() {
+ // En contexto de IRQ las interrupciones ya estan acalladas; tomar aqui
+ // el cerrojo no puede interbloquear, pues el lado tarea siempre lo toma
+ // con las interrupciones desactivadas (ver `inscribir`).
+ for waker in censo.lock().drain(..) {
+ waker.wake();
+ }
+ }
+}
+
+/// Numero de pulsos del temporizador desde el arranque.
+fn pulsos() -> u64 {
+ CONTADOR_PULSOS.load(Ordering::Acquire)
+}
+
+/// Inscribe un waker en el censo de espera del proximo fotograma.
+fn inscribir(waker: Waker) {
+ if let Some(censo) = EN_ESPERA.get() {
+ // El cerrojo lo disputa el manejador de IRQ0: tomarlo con las
+ // interrupciones acalladas hace IMPOSIBLE el interbloqueo.
+ interrupts::without_interrupts(|| censo.lock().push(waker));
+ }
+}
+
+/// Un `Future` que se resuelve en el proximo pulso del temporizador. Es la
+/// unidad de cesion cooperativa del userspace: una tarea WASM hace su trabajo
+/// de un fotograma y `.await`-ea un `EsperaFrame` para ceder hasta el siguiente.
+pub struct EsperaFrame {
+ /// Pulso a partir del cual la espera se da por cumplida.
+ objetivo: u64,
+}
+
+impl EsperaFrame {
+ /// Crea una espera que se resolvera en el siguiente pulso del temporizador.
+ pub fn nueva() -> EsperaFrame {
+ EsperaFrame {
+ objetivo: pulsos() + 1,
+ }
+ }
+}
+
+impl Future for EsperaFrame {
+ type Output = ();
+
+ fn poll(self: Pin<&mut Self>, contexto: &mut Context<'_>) -> Poll<()> {
+ // Lectura optimista: si el fotograma ya llego, no hay nada que esperar.
+ if pulsos() >= self.objetivo {
+ return Poll::Ready(());
+ }
+ // Aun no: inscribir el waker y RE-VERIFICAR. Si un pulso se colo entre
+ // la lectura de arriba y la inscripcion, este segundo chequeo lo atrapa;
+ // sin el, el despertar se perderia hasta el pulso siguiente.
+ inscribir(contexto.waker().clone());
+ if pulsos() >= self.objetivo {
+ Poll::Ready(())
+ } else {
+ Poll::Pending
+ }
+ }
+}
diff --git a/renaser/kernel/src/async_system/task.rs b/renaser/kernel/src/async_system/task.rs
new file mode 100644
index 0000000..5a4b008
--- /dev/null
+++ b/renaser/kernel/src/async_system/task.rs
@@ -0,0 +1,45 @@
+// =============================================================================
+// renaser :: async_system/task.rs — Fase 3 :: la unidad de trabajo asincrono
+// =============================================================================
+
+use alloc::boxed::Box;
+use core::future::Future;
+use core::pin::Pin;
+use core::sync::atomic::{AtomicU64, Ordering};
+use core::task::{Context, Poll};
+
+/// Identificador unico de una tarea. Autoincremental y atomico: dos tareas
+/// jamas comparten identidad.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct TaskId(u64);
+
+impl TaskId {
+ /// Acuña un identificador nuevo, distinto de todos los anteriores.
+ fn nuevo() -> TaskId {
+ static SIGUIENTE: AtomicU64 = AtomicU64::new(0);
+ TaskId(SIGUIENTE.fetch_add(1, Ordering::Relaxed))
+ }
+}
+
+/// Una tarea asincrona: un `Future` encapsulado, anclado (`Pin`) en el heap
+/// para que su direccion nunca cambie mientras avanza.
+pub struct Task {
+ /// La identidad de la tarea, su nombre ante el ejecutor.
+ pub id: TaskId,
+ futuro: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
+}
+
+impl Task {
+ /// Envuelve un `Future` en una tarea con identidad propia.
+ pub fn nueva(futuro: impl Future<Output = ()> + Send + 'static) -> Task {
+ Task {
+ id: TaskId::nuevo(),
+ futuro: Box::pin(futuro),
+ }
+ }
+
+ /// Hace avanzar la tarea un paso. `Poll::Ready` significa que concluyo.
+ pub fn poll(&mut self, contexto: &mut Context) -> Poll<()> {
+ self.futuro.as_mut().poll(contexto)
+ }
+}
diff --git a/renaser/kernel/src/async_system/teclado.rs b/renaser/kernel/src/async_system/teclado.rs
new file mode 100644
index 0000000..707d441
--- /dev/null
+++ b/renaser/kernel/src/async_system/teclado.rs
@@ -0,0 +1,76 @@
+// =============================================================================
+// renaser :: async_system/teclado.rs — el canal de scancodes del teclado
+// -----------------------------------------------------------------------------
+// El manejador de IRQ1 es un mero PRODUCTOR: deposita cada scancode en colas
+// lock-free, seguras frente a interrupciones. Los consumidores —las apps WASM,
+// via la capacidad `sys_get_scancode`— las drenan sin bloquear.
+//
+// FASE 5 :: con varias apps concurrentes, una sola cola compartida no sirve:
+// la primera en sondear le robaria la pulsacion a las demas. Por eso cada
+// aplicacion abre su PROPIO canal y la IRQ1 DIFUNDE cada scancode a todos —
+// cada app recibe su copia integra del flujo de entrada.
+// =============================================================================
+
+use alloc::sync::Arc;
+use alloc::vec::Vec;
+
+use crossbeam_queue::ArrayQueue;
+use spin::{Mutex, Once};
+use x86_64::instructions::interrupts;
+
+/// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto.
+const CAPACIDAD_COLA: usize = 256;
+
+/// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
+pub type CanalTeclado = Arc<ArrayQueue<u8>>;
+
+/// Censo de canales — uno por aplicacion del userspace. El manejador de IRQ1
+/// difunde cada scancode a TODOS: asi cada app recibe su propia copia del
+/// evento, sin que una le arrebate la pulsacion a otra.
+static CANALES: Once<Mutex<Vec<CanalTeclado>>> = Once::new();
+
+/// Funda el censo de canales del teclado. Requiere el heap ya activo; debe
+/// invocarse una sola vez, antes de habilitar las interrupciones.
+pub fn init() {
+ CANALES.call_once(|| Mutex::new(Vec::new()));
+}
+
+/// Crea un canal de teclado nuevo, AUN sin inscribir en la difusion. Cada
+/// aplicacion reclama el suyo al empezar a cargarse.
+pub fn crear_canal() -> CanalTeclado {
+ Arc::new(ArrayQueue::new(CAPACIDAD_COLA))
+}
+
+/// Inscribe un canal en el censo de difusion. Desde este instante, la IRQ1
+/// empuja cada scancode tambien a este canal. Se invoca al final de la carga
+/// de una app: una carga fallida no debe dejar canales huerfanos.
+pub fn registrar_canal(canal: &CanalTeclado) {
+ if let Some(censo) = CANALES.get() {
+ // El cerrojo lo disputa el manejador de IRQ1: tomarlo con las
+ // interrupciones acalladas hace imposible el interbloqueo.
+ interrupts::without_interrupts(|| censo.lock().push(canal.clone()));
+ }
+}
+
+/// Da de baja un canal del censo de difusion. Lo invoca el `Drop` de una
+/// aplicacion desalojada: la IRQ1 deja, de inmediato, de empujarle scancodes.
+pub fn cerrar_canal(canal: &CanalTeclado) {
+ if let Some(censo) = CANALES.get() {
+ interrupts::without_interrupts(|| {
+ censo.lock().retain(|inscrito| !Arc::ptr_eq(inscrito, canal));
+ });
+ }
+}
+
+/// Punto de entrada DESDE el manejador de IRQ1. DIFUNDE el scancode a cuantos
+/// canales haya abiertos. Deliberadamente breve y libre de panicos: corre en
+/// contexto de interrupcion.
+pub fn recibir_scancode(scancode: u8) {
+ if let Some(censo) = CANALES.get() {
+ for canal in censo.lock().iter() {
+ // Si un canal desborda, se descarta el scancode en silencio: mas
+ // vale perder una tecla que colapsar dentro de una interrupcion.
+ let _ = canal.push(scancode);
+ }
+ }
+}
diff --git a/renaser/kernel/src/async_system/waker.rs b/renaser/kernel/src/async_system/waker.rs
new file mode 100644
index 0000000..aab5449
--- /dev/null
+++ b/renaser/kernel/src/async_system/waker.rs
@@ -0,0 +1,55 @@
+// =============================================================================
+// renaser :: async_system/waker.rs — Fase 3 :: el despertador de tareas
+// -----------------------------------------------------------------------------
+// Un `Waker` es la promesa de que una tarea dormida volvera a ejecutarse. El
+// nuestro es minimo: al invocarse, reinyecta el `TaskId` de su tarea en la
+// cola de listas del ejecutor. Quien lo invoque —incluido un manejador de
+// IRQ— vuelve a poner esa tarea en circulacion.
+// =============================================================================
+
+use alloc::collections::VecDeque;
+use alloc::sync::Arc;
+use alloc::task::Wake;
+use core::task::Waker;
+
+use spin::Mutex;
+
+use super::task::TaskId;
+
+/// La cola de tareas listas para avanzar, compartida entre el ejecutor y todos
+/// los wakers que ha repartido.
+pub type ColaListas = Arc<Mutex<VecDeque<TaskId>>>;
+
+/// El despertador de UNA tarea concreta.
+struct WakerTarea {
+ id: TaskId,
+ cola: ColaListas,
+}
+
+impl WakerTarea {
+ /// Reinyecta la tarea en la cola de ejecucion.
+ fn reinyectar(&self) {
+ // El spinlock de la cola lo tocan tanto el hilo principal como los
+ // manejadores de IRQ. Tomarlo con las interrupciones acalladas hace
+ // IMPOSIBLE el interbloqueo: ninguna IRQ puede interrumpir al hilo
+ // principal justo mientras este sostiene el cerrojo.
+ x86_64::instructions::interrupts::without_interrupts(|| {
+ self.cola.lock().push_back(self.id);
+ });
+ }
+}
+
+impl Wake for WakerTarea {
+ fn wake(self: Arc<Self>) {
+ self.reinyectar();
+ }
+
+ fn wake_by_ref(self: &Arc<Self>) {
+ self.reinyectar();
+ }
+}
+
+/// Crea un `Waker` estandar que, al invocarse, reinyecta `id` en `cola`.
+pub fn crear(id: TaskId, cola: ColaListas) -> Waker {
+ Waker::from(Arc::new(WakerTarea { id, cola }))
+}
diff --git a/renaser/kernel/src/baliza.rs b/renaser/kernel/src/baliza.rs
new file mode 100644
index 0000000..01b841e
--- /dev/null
+++ b/renaser/kernel/src/baliza.rs
@@ -0,0 +1,128 @@
+// =============================================================================
+// renaser :: kernel/src/baliza.rs — la red de seguridad visual del sistema
+// -----------------------------------------------------------------------------
+// Sin consola de texto que valga cuando el sistema cae: si renaser colapsa, lo
+// DIBUJA. La baliza publica —de forma atomica y sin cerrojos— los datos
+// minimos del framebuffer, de modo que los manejadores de fallo puedan pintar
+// una franja de advertencia incluso cuando el resto del kernel ya no es fiable.
+// =============================================================================
+
+use core::panic::PanicInfo;
+use core::ptr;
+use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
+
+use crate::grafico::{escribir_pixel_volatil, Pantalla};
+
+/// Datos del framebuffer expuestos a los manejadores de fallo. Todo son
+/// atomicos: la baliza es intrinsecamente `Sync`, sin cerrojos.
+pub(crate) struct BalizaPanico {
+ base: AtomicPtr<u8>,
+ paso_bytes: AtomicUsize,
+ ancho: AtomicUsize,
+ alto: AtomicUsize,
+ bytes_por_pixel: AtomicUsize,
+ /// Rojo de alerta de colapso, ya codificado al formato de la pantalla.
+ pixel_alerta: AtomicU32,
+ /// Naranja de agotamiento de memoria, ya codificado.
+ pixel_oom: AtomicU32,
+}
+
+impl BalizaPanico {
+ /// Baliza apagada: sin pantalla publicada todavia.
+ const fn apagada() -> BalizaPanico {
+ BalizaPanico {
+ base: AtomicPtr::new(ptr::null_mut()),
+ paso_bytes: AtomicUsize::new(0),
+ ancho: AtomicUsize::new(0),
+ alto: AtomicUsize::new(0),
+ bytes_por_pixel: AtomicUsize::new(0),
+ pixel_alerta: AtomicU32::new(0),
+ pixel_oom: AtomicU32::new(0),
+ }
+ }
+
+ /// Enciende la baliza con los datos de una pantalla viva.
+ pub(crate) fn encender(&self, pantalla: &Pantalla, pixel_alerta: u32, pixel_oom: u32) {
+ self.paso_bytes.store(pantalla.paso_bytes, Ordering::Relaxed);
+ self.ancho.store(pantalla.ancho, Ordering::Relaxed);
+ self.alto.store(pantalla.alto, Ordering::Relaxed);
+ self.bytes_por_pixel
+ .store(pantalla.bytes_por_pixel, Ordering::Relaxed);
+ self.pixel_alerta.store(pixel_alerta, Ordering::Relaxed);
+ self.pixel_oom.store(pixel_oom, Ordering::Relaxed);
+ // La base se publica de ultima, con semantica `Release`.
+ self.base.store(pantalla.base, Ordering::Release);
+ }
+
+ /// Pinta, directa y volatilmente, una banda horizontal sobre el framebuffer
+ /// fisico. Es la herramienta de los manejadores de fallo: no confia ni en
+ /// el lienzo, ni en el heap, ni en estructura dinamica alguna.
+ fn pintar_banda(&self, y0: usize, altura: usize, pixel: u32) {
+ let base = self.base.load(Ordering::Acquire);
+ if base.is_null() {
+ return;
+ }
+ let ancho = self.ancho.load(Ordering::Relaxed);
+ let alto = self.alto.load(Ordering::Relaxed);
+ let paso = self.paso_bytes.load(Ordering::Relaxed);
+ let bpp = self.bytes_por_pixel.load(Ordering::Relaxed);
+
+ let y_fin = (y0 + altura).min(alto);
+ let mut y = y0.min(alto);
+ while y < y_fin {
+ let fila = y * paso;
+ let mut x = 0;
+ while x < ancho {
+ // SEGURIDAD: (x, y) esta acotado por las dimensiones que la
+ // baliza publico desde una pantalla real.
+ unsafe {
+ escribir_pixel_volatil(base.add(fila + x * bpp), pixel, bpp);
+ }
+ x += 1;
+ }
+ y += 1;
+ }
+ }
+
+ /// Altura de la franja de advertencia: ~8 % de la pantalla.
+ fn franja(&self) -> usize {
+ let alto = self.alto.load(Ordering::Relaxed);
+ if alto == 0 {
+ 0
+ } else {
+ (alto / 12).max(1)
+ }
+ }
+}
+
+/// Instancia global de la baliza. Comienza apagada y se enciende en el arranque.
+pub(crate) static BALIZA_PANICO: BalizaPanico = BalizaPanico::apagada();
+
+// =============================================================================
+// MANEJADORES DE FALLO — cuando el sistema colapsa, lo DIBUJA
+// =============================================================================
+
+/// Si renaser colapsa, tatuamos una franja ROJA en lo alto del framebuffer.
+#[panic_handler]
+fn al_colapsar(_info: &PanicInfo) -> ! {
+ x86_64::instructions::interrupts::disable();
+ BALIZA_PANICO.pintar_banda(
+ 0,
+ BALIZA_PANICO.franja(),
+ BALIZA_PANICO.pixel_alerta.load(Ordering::Relaxed),
+ );
+ crate::detener()
+}
+
+/// Si el heap se agota, tatuamos una franja NARANJA: un fallo distinto al
+/// colapso, y distinguible de un vistazo.
+#[alloc_error_handler]
+fn al_agotar_memoria(_disposicion: core::alloc::Layout) -> ! {
+ x86_64::instructions::interrupts::disable();
+ BALIZA_PANICO.pintar_banda(
+ 0,
+ BALIZA_PANICO.franja(),
+ BALIZA_PANICO.pixel_oom.load(Ordering::Relaxed),
+ );
+ crate::detener()
+}
diff --git a/renaser/kernel/src/consola.rs b/renaser/kernel/src/consola.rs
new file mode 100644
index 0000000..68796a6
--- /dev/null
+++ b/renaser/kernel/src/consola.rs
@@ -0,0 +1,186 @@
+// =============================================================================
+// renaser :: kernel/src/consola.rs — la superficie de texto e imagen
+// -----------------------------------------------------------------------------
+// La consola une el lienzo intermedio, la pantalla fisica y una pluma de
+// escritura. Rasteriza cada glifo con `fontdue` al vuelo —el texto convertido,
+// por fin, en dibujo— y tambien sabe volcar fotogramas crudos del userspace
+// WASM. Es global y serializada tras un `Mutex`: las tareas escriben en ella.
+// =============================================================================
+
+use spin::{Mutex, Once};
+
+use crate::grafico::{codificar, Color, Lienzo, Pantalla, RegionPantalla};
+use crate::texto;
+
+/// Margen del texto respecto al borde del lienzo, en pixeles.
+const MARGEN: usize = 40;
+/// Tamaño de la tipografia, en pixeles.
+const TAM_FUENTE: f32 = 30.0;
+/// Altura de avance de una linea de texto, en pixeles.
+const ALTO_LINEA: usize = 40;
+/// Posicion vertical de la primera linea base.
+const BASE_INICIAL: usize = MARGEN + 30;
+
+/// Interpola dos colores segun una cobertura: `0` => `fondo`, `255` => `tinta`.
+fn mezclar(fondo: Color, tinta: Color, cobertura: u8) -> Color {
+ let canal = |a: u8, b: u8| -> u8 {
+ let c = cobertura as u16;
+ ((a as u16 * (255 - c) + b as u16 * c) / 255) as u8
+ };
+ Color {
+ r: canal(fondo.r, tinta.r),
+ g: canal(fondo.g, tinta.g),
+ b: canal(fondo.b, tinta.b),
+ }
+}
+
+/// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
+pub(crate) struct Consola {
+ lienzo: Lienzo,
+ pantalla: Pantalla,
+ /// Posicion horizontal de la pluma de escritura.
+ pluma_x: usize,
+ /// Linea base vertical de la pluma de escritura.
+ base_y: usize,
+}
+
+// SEGURIDAD: `Consola` encierra, via `Pantalla`, un puntero crudo al
+// framebuffer. Ese puntero es valido durante toda la vida del kernel y todo
+// acceso a la consola se serializa tras un `Mutex`. En un sistema de un solo
+// nucleo, esto la hace segura de compartir entre el hilo principal y las tareas.
+unsafe impl Send for Consola {}
+
+impl Consola {
+ /// Crea una consola con la pluma en la esquina superior izquierda.
+ pub(crate) fn nueva(lienzo: Lienzo, pantalla: Pantalla) -> Consola {
+ Consola {
+ lienzo,
+ pantalla,
+ pluma_x: MARGEN,
+ base_y: BASE_INICIAL,
+ }
+ }
+
+ /// Lleva la pluma al inicio de la siguiente linea. Al llegar al fondo,
+ /// limpia el lienzo: una pizarra nueva.
+ fn nueva_linea(&mut self) {
+ self.pluma_x = MARGEN;
+ self.base_y += ALTO_LINEA;
+ if self.base_y + ALTO_LINEA >= self.lienzo.alto {
+ self.lienzo.limpiar(Color::LIENZO_EN_REPOSO);
+ self.base_y = BASE_INICIAL;
+ }
+ }
+
+ /// Escribe un caracter: rasteriza su glifo y avanza la pluma.
+ fn escribir_char(&mut self, caracter: char) {
+ if caracter == '\n' {
+ self.nueva_linea();
+ return;
+ }
+ let (metricas, cobertura) = texto::rasterizar(caracter, TAM_FUENTE);
+ // Salto de linea automatico al alcanzar el margen derecho.
+ if self.pluma_x + metricas.advance_width as usize + MARGEN > self.lienzo.ancho {
+ self.nueva_linea();
+ }
+ self.dibujar_glifo(&metricas, &cobertura);
+ self.pluma_x += metricas.advance_width as usize;
+ }
+
+ /// Escribe una cadena completa, caracter a caracter.
+ pub(crate) fn escribir(&mut self, texto: &str) {
+ for caracter in texto.chars() {
+ self.escribir_char(caracter);
+ }
+ }
+
+ /// Funde un mapa de cobertura de `fontdue` sobre el lienzo, en la pluma.
+ fn dibujar_glifo(&mut self, metricas: &fontdue::Metrics, cobertura: &[u8]) {
+ // Origen del glifo: la pluma desplazada por las metricas. El mapa de
+ // `fontdue` se recorre de arriba a abajo desde la cima del glifo.
+ let inicio_x = self.pluma_x as isize + metricas.xmin as isize;
+ let inicio_y = self.base_y as isize - metricas.ymin as isize - metricas.height as isize;
+
+ for fila in 0..metricas.height {
+ for col in 0..metricas.width {
+ let opacidad = cobertura[fila * metricas.width + col];
+ if opacidad == 0 {
+ continue; // pixel transparente: no toca el fondo
+ }
+ let x = inicio_x + col as isize;
+ let y = inicio_y + fila as isize;
+ if x < 0 || y < 0 {
+ continue;
+ }
+ let color = mezclar(Color::LIENZO_EN_REPOSO, Color::TEXTO, opacidad);
+ self.lienzo.pintar_pixel(x as usize, y as usize, color);
+ }
+ }
+ }
+
+ /// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
+ /// sus limites ya verificados por el host— sobre la SUB-REGION asignada a su
+ /// aplicacion. Cada pixel se recodifica al formato nativo del framebuffer y
+ /// se deposita desplazado por `(region.x, region.y)`: una app jamas escribe
+ /// fuera de su ventana, y varias cohabitan el lienzo sin pisarse.
+ fn volcar_marco(&mut self, region: RegionPantalla, datos: &[u8]) {
+ for (indice, trozo) in datos.chunks_exact(4).enumerate() {
+ let columna = indice % region.ancho;
+ let fila = indice / region.ancho;
+ if fila >= region.alto {
+ break; // el fotograma excede el alto de la region: se ignora el resto
+ }
+ let x = region.x + columna;
+ let y = region.y + fila;
+ if x >= self.lienzo.ancho || y >= self.lienzo.alto {
+ continue; // recorte firme: nada se pinta fuera del lienzo
+ }
+ let p = u32::from_le_bytes([trozo[0], trozo[1], trozo[2], trozo[3]]);
+ let color = Color {
+ r: (p >> 16) as u8,
+ g: (p >> 8) as u8,
+ b: p as u8,
+ };
+ self.lienzo.pixeles[y * self.lienzo.ancho + x] =
+ codificar(self.lienzo.formato, color);
+ }
+ self.presentar();
+ }
+
+ /// Inunda una region entera con un color plano y la presenta. Es la baliza
+ /// de desalojo: cuando una aplicacion falla, su ventana se tatua de purpura.
+ fn pintar_region(&mut self, region: RegionPantalla, color: Color) {
+ self.lienzo
+ .rellenar_rect(region.x, region.y, region.ancho, region.alto, color);
+ self.presentar();
+ }
+
+ /// Vuelca el lienzo sobre la pantalla fisica.
+ pub(crate) fn presentar(&mut self) {
+ self.pantalla.presentar(&self.lienzo);
+ }
+}
+
+/// La consola global de renaser. Se funde en el arranque; despues, las tareas
+/// asincronas y las capacidades del userspace escriben en ella tras su `Mutex`.
+pub(crate) static CONSOLA: Once<Mutex<Consola>> = Once::new();
+
+/// Puerta del kernel para la capacidad `sys_render_frame` del userspace WASM:
+/// compone sobre la consola global un fotograma —cuyos limites el host ya
+/// verifico matematicamente contra la memoria lineal del modulo— dentro de la
+/// region de pantalla que el kernel asigno a esa aplicacion.
+pub(crate) fn volcar_marco_wasm(region: RegionPantalla, datos: &[u8]) {
+ if let Some(consola) = CONSOLA.get() {
+ consola.lock().volcar_marco(region, datos);
+ }
+}
+
+/// Tatua la baliza de desalojo sobre la region de una aplicacion que el kernel
+/// ha dado por terminada. El color delata la causa —purpura para una falla de
+/// ejecucion o de combustible, amarillo palido para un desbordo de memoria—. Es
+/// una advertencia NO fatal: la app muere, el kernel y sus vecinas siguen vivos.
+pub(crate) fn pintar_desalojo(region: RegionPantalla, color: Color) {
+ if let Some(consola) = CONSOLA.get() {
+ consola.lock().pintar_region(region, color);
+ }
+}
diff --git a/renaser/kernel/src/drivers/disco.rs b/renaser/kernel/src/drivers/disco.rs
new file mode 100644
index 0000000..e18f07a
--- /dev/null
+++ b/renaser/kernel/src/drivers/disco.rs
@@ -0,0 +1,562 @@
+// =============================================================================
+// renaser :: kernel/src/drivers/disco.rs — Fase 6.2 :: el disco asincrono
+// -----------------------------------------------------------------------------
+// La Fase 6.1 hizo hablar al disco; pero lo hacia por SONDEO: el procesador se
+// quedaba en un bucle de espera activa vigilando el «used ring» de virtio,
+// incapaz de atender nada mas. La Fase 6.2 lo libera. La E/S de bloques pasa a
+// ser REACTIVA, guiada por la interrupcion fisica del dispositivo:
+//
+// * `EsperaDisco` — un `Future` nativo: enviada la peticion, cede la CPU; la
+// IRQ del disco lo despertara cuando el bloque este listo.
+// * `atender_irq` — el punto al que salta el manejador de la IRQ del disco:
+// reconoce la interrupcion en el dispositivo y despierta a quien aguardaba.
+// * `bloquear_en` — el puente para los contextos SINCRONOS (el arranque, las
+// capacidades WASM): lleva un `Future` de disco hasta su final durmiendo la
+// CPU con `hlt` —jamas en espera activa una vez el sistema esta en marcha—.
+//
+// Subsisten de la Fase 6.1 el asignador de marcos por mapa de bits (con
+// liberacion real) y `KernelHal`, el puente DMA hacia `virtio-drivers`.
+// =============================================================================
+
+use alloc::boxed::Box;
+use alloc::vec;
+use alloc::vec::Vec;
+use core::future::Future;
+use core::pin::Pin;
+use core::ptr::NonNull;
+use core::sync::atomic::{AtomicU64, AtomicU8, Ordering};
+use core::task::{Context, Poll, Waker};
+
+use spin::{Mutex, Once};
+use virtio_drivers::device::blk::{BlkReq, BlkResp, VirtIOBlk, SECTOR_SIZE};
+use virtio_drivers::transport::pci::bus::{Command, DeviceFunction, PciRoot};
+use virtio_drivers::transport::pci::PciTransport;
+use virtio_drivers::{BufferDirection, Hal, PhysAddr};
+use x86_64::instructions::interrupts;
+
+use super::pci::CamPuertos;
+
+/// Tamaño de una pagina / marco fisico, en bytes.
+const PAGINA: u64 = 4096;
+
+/// Techo de marcos que gestiona el asignador de DMA: 4096 marcos => una arena
+/// de 16 MiB. El DMA del disco —colas virtio y buferes rebote— necesita una
+/// fraccion minima de eso; el techo solo acota el tamaño del mapa de bits.
+const MAX_MARCOS: usize = 4096;
+
+/// Vendor ID de VirtIO; Device IDs de un disco de bloques (transicional/moderno).
+const VENDOR_VIRTIO: u16 = 0x1AF4;
+const VIRTIO_BLK_IDS: [u16; 2] = [0x1001, 0x1042];
+
+/// El tamaño de un sector, reexportado para el resto del kernel.
+pub const TAM_SECTOR: usize = SECTOR_SIZE;
+
+// =============================================================================
+// EL OFFSET DE MEMORIA FISICA Y EL ASIGNADOR DE MARCOS
+// =============================================================================
+
+/// Desplazamiento al que el cargador mapeo toda la memoria fisica: una
+/// direccion fisica `p` es accesible por el kernel en la virtual `p + offset`.
+static OFFSET_FISICO: AtomicU64 = AtomicU64::new(0);
+
+/// Asignador de marcos por MAPA DE BITS: gestiona una arena de RAM fisica
+/// contigua y LIBERA. Cada bit representa un marco de 4 KiB; `1` es ocupado,
+/// `0` libre. Un almacen de objetos vivo asigna y devuelve marcos sin descanso.
+struct AsignadorMarcos {
+ /// Direccion fisica del primer marco gestionado, alineada a pagina.
+ base: u64,
+ /// Numero de marcos que abarca la arena.
+ total: usize,
+ /// Mapa de ocupacion: un bit por marco.
+ mapa: Vec<u64>,
+}
+
+impl AsignadorMarcos {
+ /// ¿Esta libre el marco `i`?
+ fn libre(&self, i: usize) -> bool {
+ (self.mapa[i / 64] >> (i % 64)) & 1 == 0
+ }
+
+ /// Marca el marco `i` como ocupado.
+ fn ocupar(&mut self, i: usize) {
+ self.mapa[i / 64] |= 1 << (i % 64);
+ }
+
+ /// Marca el marco `i` como libre.
+ fn soltar(&mut self, i: usize) {
+ self.mapa[i / 64] &= !(1u64 << (i % 64));
+ }
+
+ /// Reserva `paginas` marcos CONTIGUOS y devuelve la direccion fisica del
+ /// primero. `None` si no hay un hueco contiguo bastante grande.
+ fn asignar(&mut self, paginas: usize) -> Option<u64> {
+ if paginas == 0 || paginas > self.total {
+ return None;
+ }
+ let mut i = 0;
+ while i + paginas <= self.total {
+ // Buscar `paginas` marcos libres consecutivos a partir de `i`.
+ match (i..i + paginas).find(|&k| !self.libre(k)) {
+ // Un marco ocupado rompe la racha: reanudar tras el.
+ Some(ocupado) => i = ocupado + 1,
+ // Racha completa: ocuparla y entregar su direccion fisica.
+ None => {
+ for k in i..i + paginas {
+ self.ocupar(k);
+ }
+ return Some(self.base + (i as u64) * PAGINA);
+ }
+ }
+ }
+ None
+ }
+
+ /// Devuelve a la arena `paginas` marcos que arrancan en la direccion
+ /// fisica `fisica`. Direcciones ajenas a la arena se ignoran sin daño.
+ fn liberar(&mut self, fisica: u64, paginas: usize) {
+ if fisica < self.base {
+ return;
+ }
+ let inicio = ((fisica - self.base) / PAGINA) as usize;
+ for k in inicio..(inicio + paginas).min(self.total) {
+ self.soltar(k);
+ }
+ }
+}
+
+/// El asignador global de marcos para DMA. Se funde en `init`.
+static ASIGNADOR: Once<Mutex<AsignadorMarcos>> = Once::new();
+
+/// Funda el subsistema de disco: registra el offset de memoria fisica y forja
+/// el asignador de marcos sobre la region de RAM libre que el cargador reporto.
+/// Una sola vez, antes de montar el disco.
+pub fn init(offset_fisico: u64, region_inicio: u64, region_fin: u64) {
+ OFFSET_FISICO.store(offset_fisico, Ordering::Relaxed);
+ let base = alinear_arriba(region_inicio, PAGINA);
+ let disponibles = region_fin.saturating_sub(base) / PAGINA;
+ let total = (disponibles as usize).min(MAX_MARCOS);
+ ASIGNADOR.call_once(|| {
+ Mutex::new(AsignadorMarcos {
+ base,
+ total,
+ mapa: vec![0u64; total.div_ceil(64)],
+ })
+ });
+}
+
+/// Redondea `valor` hacia arriba al multiplo de `alineacion` (potencia de dos).
+fn alinear_arriba(valor: u64, alineacion: u64) -> u64 {
+ (valor + alineacion - 1) & !(alineacion - 1)
+}
+
+/// Reserva `paginas` marcos fisicos contiguos de 4 KiB y devuelve su direccion
+/// fisica. Agotar la arena es un fallo del kernel, no recuperable aqui: el
+/// rasgo `Hal` no admite que `dma_alloc` falle.
+fn asignar_marcos(paginas: usize) -> u64 {
+ ASIGNADOR
+ .get()
+ .expect("DMA :: el asignador de marcos no esta fundado")
+ .lock()
+ .asignar(paginas)
+ .expect("DMA :: la arena de marcos fisicos se agoto")
+}
+
+/// Devuelve `paginas` marcos fisicos a la arena. El reverso de `asignar_marcos`.
+fn liberar_marcos(fisica: u64, paginas: usize) {
+ if let Some(asignador) = ASIGNADOR.get() {
+ asignador.lock().liberar(fisica, paginas);
+ }
+}
+
+/// Traduce una direccion fisica a la virtual que el kernel puede desreferenciar.
+fn a_virtual(fisica: u64) -> *mut u8 {
+ (fisica + OFFSET_FISICO.load(Ordering::Relaxed)) as *mut u8
+}
+
+// =============================================================================
+// KernelHal — EL PUENTE ENTRE `virtio-drivers` Y LA MEMORIA DE renaser
+// =============================================================================
+
+/// La implementacion del rasgo `Hal` de `virtio-drivers`. Sin estado propio:
+/// se apoya en el asignador de marcos y el offset fisico, ambos globales.
+pub struct KernelHal;
+
+// SEGURIDAD: cada metodo respeta su contrato — `dma_alloc` entrega marcos
+// fisicos exclusivos, contiguos, alineados a pagina y a cero; `dma_dealloc` y
+// `unshare` los devuelven a la arena; las traducciones de direccion son validas
+// porque el cargador mapeo toda la memoria fisica.
+unsafe impl Hal for KernelHal {
+ fn dma_alloc(paginas: usize, _direccion: BufferDirection) -> (PhysAddr, NonNull<u8>) {
+ let fisica = asignar_marcos(paginas);
+ let virtual_ = a_virtual(fisica);
+ // SEGURIDAD: `asignar_marcos` entrego `paginas` marcos exclusivos y
+ // contiguos; `virtual_` es su imagen valida y escribible en el mapeo
+ // de memoria fisica. El rasgo exige que las paginas vayan a cero.
+ unsafe {
+ core::ptr::write_bytes(virtual_, 0, paginas * PAGINA as usize);
+ }
+ (fisica, NonNull::new(virtual_).expect("DMA :: marco fisico nulo"))
+ }
+
+ unsafe fn dma_dealloc(fisica: PhysAddr, _virtual_: NonNull<u8>, paginas: usize) -> i32 {
+ // Con un asignador real, la liberacion ya NO es un gesto vacio: los
+ // marcos vuelven a la arena y quedan disponibles para el proximo DMA.
+ liberar_marcos(fisica, paginas);
+ 0
+ }
+
+ unsafe fn mmio_phys_to_virt(fisica: PhysAddr, _tam: usize) -> NonNull<u8> {
+ // El cargador mapeo, como minimo, los primeros 4 GiB de memoria fisica;
+ // todo BAR MMIO de PCI cae dentro y es accesible en `fisica + offset`.
+ NonNull::new(a_virtual(fisica)).expect("MMIO :: direccion fisica nula")
+ }
+
+ unsafe fn share(bufer: NonNull<[u8]>, direccion: BufferDirection) -> PhysAddr {
+ let longitud = bufer.len();
+ let paginas = longitud.div_ceil(PAGINA as usize);
+ let fisica = asignar_marcos(paginas);
+ // Si el bufer viaja HACIA el dispositivo, copiarlo al area DMA rebote.
+ if !matches!(direccion, BufferDirection::DeviceToDriver) {
+ // SEGURIDAD: el rasgo garantiza que `bufer` apunta a `longitud`
+ // bytes validos; el area DMA recien reservada los abarca de sobra.
+ unsafe {
+ core::ptr::copy_nonoverlapping(
+ bufer.as_ptr() as *const u8,
+ a_virtual(fisica),
+ longitud,
+ );
+ }
+ }
+ fisica
+ }
+
+ unsafe fn unshare(fisica: PhysAddr, bufer: NonNull<[u8]>, direccion: BufferDirection) {
+ let longitud = bufer.len();
+ // Si el bufer viene DESDE el dispositivo, copiar el area DMA de vuelta.
+ if !matches!(direccion, BufferDirection::DriverToDevice) {
+ // SEGURIDAD: `fisica` lo entrego `share` para este mismo `bufer`;
+ // ambas regiones abarcan los `longitud` bytes que se copian.
+ unsafe {
+ core::ptr::copy_nonoverlapping(
+ a_virtual(fisica),
+ bufer.as_ptr() as *mut u8,
+ longitud,
+ );
+ }
+ }
+ // Devolver a la arena los marcos del area rebote.
+ liberar_marcos(fisica, longitud.div_ceil(PAGINA as usize));
+ }
+}
+
+// =============================================================================
+// EL DISCO PERSISTENTE
+// =============================================================================
+
+/// El disco virtio-blk, ya montado. Envuelve al `VirtIOBlk` para poder ligarlo
+/// a un `static`.
+struct Disco(VirtIOBlk<KernelHal, PciTransport>);
+
+// SEGURIDAD: `Disco` encierra punteros crudos a las colas virtio y al MMIO del
+// dispositivo. renaser es un kernel de un solo nucleo y todo acceso al disco se
+// serializa tras el `Mutex` global `DISCO`; jamas se comparte entre hilos
+// reales. Todo acceso normal toma el cerrojo con las interrupciones acalladas,
+// de modo que la IRQ del disco jamas lo encuentra ocupado.
+unsafe impl Send for Disco {}
+
+/// El disco global de renaser. Se monta una sola vez, en `montar`.
+static DISCO: Once<Mutex<Disco>> = Once::new();
+
+/// La linea de IRQ del disco, descubierta al montarlo. Vale 0 si el firmware no
+/// enruto una linea util: en ese caso la E/S recae en el sondeo, con la red de
+/// seguridad del temporizador.
+static IRQ_DISCO: AtomicU8 = AtomicU8::new(0);
+
+/// Cuenta de interrupciones del disco atendidas desde el arranque. Es el
+/// testigo vivo de que la E/S asincrona late de verdad.
+static PULSOS_DISCO: AtomicU64 = AtomicU64::new(0);
+
+/// El waker de la (unica) espera de disco en curso. Las operaciones de disco se
+/// serializan, de modo que una sola ranura basta. La IRQ del disco lo invoca.
+static WAKER_DISCO: Mutex<Option<Waker>> = Mutex::new(None);
+
+/// Enumera el bus PCI, localiza el disco virtio-blk, lo monta y lo deja tras el
+/// `Mutex` global. Descubre ademas su linea de IRQ, registra el manejador y
+/// abre la linea en el PIC: desde aqui el disco puede interrumpir. Devuelve la
+/// capacidad del disco en sectores. Toda falla se devuelve como `Err`.
+pub fn montar() -> Result<u64, &'static str> {
+ let mut raiz = PciRoot::new(CamPuertos);
+
+ // 1. Localizar el primer disco virtio-blk recorriendo el bus.
+ let mut hallado: Option<DeviceFunction> = None;
+ 'busqueda: for bus in 0..=255u8 {
+ for (device_function, info) in raiz.enumerate_bus(bus) {
+ if info.vendor_id == VENDOR_VIRTIO && VIRTIO_BLK_IDS.contains(&info.device_id) {
+ hallado = Some(device_function);
+ break 'busqueda;
+ }
+ }
+ }
+ let device_function = hallado.ok_or("virtio-blk no hallado en el bus PCI")?;
+
+ // 2. Habilitar E/S, espacio de memoria y BUS-MASTER. Sin bus-master el
+ // dispositivo no puede iniciar transferencias DMA por su cuenta.
+ raiz.set_command(
+ device_function,
+ Command::IO_SPACE | Command::MEMORY_SPACE | Command::BUS_MASTER,
+ );
+
+ // 3. Montar el transporte PCI moderno de virtio y el dispositivo de bloques.
+ let transporte = PciTransport::new::<KernelHal, _>(&mut raiz, device_function)
+ .map_err(|_| "no se pudo montar el transporte PCI de virtio")?;
+ let mut disco = VirtIOBlk::<KernelHal, _>::new(transporte)
+ .map_err(|_| "no se pudo inicializar el dispositivo virtio-blk")?;
+ let capacidad = disco.capacity();
+
+ // 4. Pedir al dispositivo que EMITA una interrupcion al completar cada
+ // peticion — el corazon de la E/S asincrona de esta fase.
+ disco.enable_interrupts();
+ DISCO.call_once(|| Mutex::new(Disco(disco)));
+
+ // 5. Descubrir la linea de IRQ que el firmware asigno al dispositivo y
+ // enrutarla: registrar el manejador en la IDT y abrir la linea en el
+ // PIC. Las IRQ 0 y 1 son del temporizador y el teclado; un valor fuera
+ // de 2..15 (p. ej. 0xFF, «sin conexion») significa que no hay linea
+ // util — la E/S seguira funcionando, pero por sondeo.
+ let irq = super::pci::linea_irq(device_function);
+ if (2..=15).contains(&irq) {
+ crate::interrupts::registrar_irq_disco(irq);
+ crate::pic::desenmascarar(irq);
+ IRQ_DISCO.store(irq, Ordering::SeqCst);
+ }
+
+ Ok(capacidad)
+}
+
+/// La linea de IRQ del disco, si el firmware enruto una util.
+pub fn irq() -> Option<u8> {
+ match IRQ_DISCO.load(Ordering::SeqCst) {
+ 0 => None,
+ n => Some(n),
+ }
+}
+
+/// Numero de interrupciones del disco atendidas desde el arranque.
+pub fn pulsos_disco() -> u64 {
+ PULSOS_DISCO.load(Ordering::Relaxed)
+}
+
+/// Punto de entrada DESDE el manejador de la IRQ del disco. Reconoce la
+/// interrupcion en el dispositivo —leer su registro ISR baja la linea INTx— y
+/// despierta a la tarea que aguardaba el bloque. Breve y libre de panicos:
+/// corre en contexto de interrupcion.
+pub fn atender_irq() {
+ // Estamos en contexto de interrupcion (IF=0). Todo acceso normal a `DISCO`
+ // toma su cerrojo con las interrupciones acalladas, de modo que aqui jamas
+ // esta ocupado: tomarlo no puede interbloquear.
+ if let Some(disco) = DISCO.get() {
+ let _ = disco.lock().0.ack_interrupt();
+ }
+ PULSOS_DISCO.fetch_add(1, Ordering::Relaxed);
+ if let Some(waker) = WAKER_DISCO.lock().take() {
+ waker.wake();
+ }
+}
+
+/// Inscribe el waker de la espera de disco en curso. Una sola ranura: las
+/// operaciones de disco estan serializadas. El cerrojo lo disputa el manejador
+/// de IRQ — tomarlo con las interrupciones acalladas hace imposible el bloqueo.
+fn registrar_waker(waker: Waker) {
+ interrupts::without_interrupts(|| *WAKER_DISCO.lock() = Some(waker));
+}
+
+// =============================================================================
+// EsperaDisco — UNA OPERACION DE BLOQUE COMO `Future`
+// =============================================================================
+
+/// Una transferencia de bloques en vuelo, expresada como `Future`. Posee sus
+/// propios buferes DMA —el encabezado de peticion, la respuesta y los datos—,
+/// que `virtio-drivers` mantiene prestados hasta que la operacion concluye.
+///
+/// Su `poll` envia la peticion la primera vez y, despues, comprueba el «used
+/// ring»: si el dispositivo aun no ha terminado, inscribe el waker y cede; la
+/// IRQ del disco lo reanudara. Una operacion a la vez — basta para renaser.
+pub struct EsperaDisco {
+ /// Encabezado de la peticion virtio. En el heap: direccion estable mientras
+ /// el dispositivo lo tiene prestado, sin importar si el `Future` se mueve.
+ req: Box<BlkReq>,
+ /// Respuesta de estado del dispositivo. En el heap, por la misma razon.
+ resp: Box<BlkResp>,
+ /// Los datos: destino de una lectura, origen de una escritura.
+ buf: Vec<u8>,
+ /// Primer sector de la transferencia.
+ sector: u64,
+ /// `true` si la operacion escribe; `false` si lee.
+ es_escritura: bool,
+ /// Token que `virtio-drivers` devolvio al enviar la peticion. `None` hasta
+ /// que el primer `poll` la envia.
+ token: Option<u16>,
+}
+
+impl EsperaDisco {
+ /// Hace avanzar la operacion: la envia si aun no lo estaba y comprueba si el
+ /// dispositivo la completo. `None` => sigue en vuelo; `Some` => terminada.
+ fn avanzar(&mut self) -> Option<Result<Vec<u8>, &'static str>> {
+ // Todo el dialogo con el dispositivo, con las interrupciones acalladas:
+ // asi la IRQ del disco jamas encuentra ocupado el cerrojo de `DISCO`.
+ interrupts::without_interrupts(|| {
+ let disco = match DISCO.get() {
+ Some(disco) => disco,
+ None => return Some(Err("disco no montado")),
+ };
+ let mut guardia = disco.lock();
+ let blk = &mut guardia.0;
+
+ // 1. Enviar la peticion la primera vez que se sondea esta espera.
+ if self.token.is_none() {
+ // SEGURIDAD: `req`, `buf` y `resp` viven en esta `EsperaDisco`,
+ // que no se libera ni se accede por otra via hasta que el
+ // `complete_*` de mas abajo cierre la operacion.
+ let envio = unsafe {
+ if self.es_escritura {
+ blk.write_blocks_nb(
+ self.sector as usize,
+ &mut self.req,
+ self.buf.as_slice(),
+ &mut self.resp,
+ )
+ } else {
+ blk.read_blocks_nb(
+ self.sector as usize,
+ &mut self.req,
+ self.buf.as_mut_slice(),
+ &mut self.resp,
+ )
+ }
+ };
+ match envio {
+ Ok(token) => self.token = Some(token),
+ Err(_) => return Some(Err("no se pudo enviar la peticion al disco")),
+ }
+ }
+ let token = self.token.unwrap();
+
+ // 2. ¿Ha colocado el dispositivo este token en el «used ring»?
+ if blk.peek_used() != Some(token) {
+ return None; // aun en vuelo
+ }
+
+ // 3. Completado: recoger el resultado y desligar los buferes DMA.
+ // SEGURIDAD: se pasan los MISMOS buferes que recibio el `*_nb`.
+ let fin = unsafe {
+ if self.es_escritura {
+ blk.complete_write_blocks(token, &self.req, self.buf.as_slice(), &mut self.resp)
+ } else {
+ blk.complete_read_blocks(
+ token,
+ &self.req,
+ self.buf.as_mut_slice(),
+ &mut self.resp,
+ )
+ }
+ };
+ Some(match fin {
+ Ok(()) => Ok(core::mem::take(&mut self.buf)),
+ Err(_) => Err("la operacion de disco no se completo"),
+ })
+ })
+ }
+}
+
+impl Future for EsperaDisco {
+ /// Al terminar, una lectura entrega sus datos; una escritura, un vector
+ /// vacio. El error es siempre un mensaje estable.
+ type Output = Result<Vec<u8>, &'static str>;
+
+ fn poll(self: Pin<&mut Self>, contexto: &mut Context<'_>) -> Poll<Self::Output> {
+ // `EsperaDisco` es `Unpin` —solo `Box`, `Vec` y escalares—: el `Pin`
+ // no impone nada y el acceso mutable es directo.
+ let this = self.get_mut();
+ // Inscribir el waker ANTES de comprobar: si la IRQ se cuela entre la
+ // comprobacion y la inscripcion, el waker ya esta puesto y el despertar
+ // no se pierde — el mismo blindaje que usa `EsperaFrame` (ver `reloj`).
+ registrar_waker(contexto.waker().clone());
+ match this.avanzar() {
+ Some(resultado) => Poll::Ready(resultado),
+ None => Poll::Pending,
+ }
+ }
+}
+
+/// Prepara la LECTURA asincrona de `n_sectores` sectores desde `sector`. El
+/// `Future` que devuelve no toca el disco hasta que se le sondea.
+pub fn leer_bloques(sector: u64, n_sectores: usize) -> EsperaDisco {
+ EsperaDisco {
+ req: Box::new(BlkReq::default()),
+ resp: Box::new(BlkResp::default()),
+ buf: vec![0u8; n_sectores * TAM_SECTOR],
+ sector,
+ es_escritura: false,
+ token: None,
+ }
+}
+
+/// Prepara la ESCRITURA asincrona de `datos` a partir de `sector`. La longitud
+/// de `datos` debe ser multiplo de un sector.
+pub fn escribir_bloques(sector: u64, datos: Vec<u8>) -> EsperaDisco {
+ EsperaDisco {
+ req: Box::new(BlkReq::default()),
+ resp: Box::new(BlkResp::default()),
+ buf: datos,
+ sector,
+ es_escritura: true,
+ token: None,
+ }
+}
+
+// =============================================================================
+// bloquear_en — EL PUENTE PARA LOS CONTEXTOS SINCRONOS
+// =============================================================================
+
+/// Lleva un `Future` de disco hasta su final desde un contexto SINCRONO — el
+/// arranque, o una capacidad WASM, que no pueden `.await`-ear—. Mientras el
+/// disco trabaja:
+///
+/// * si las interrupciones estan activas, duerme la CPU con `hlt`; la
+/// despertara la IRQ del disco —o el temporizador, como red de seguridad—;
+/// * si no —en el arranque, antes de habilitarlas—, sondea en bucle.
+///
+/// Asi, una vez el sistema esta en marcha, la espera de disco JAMAS malgasta
+/// ciclos en espera activa.
+fn bloquear_en<F: Future>(futuro: F) -> F::Output {
+ let mut futuro = core::pin::pin!(futuro);
+ let waker = Waker::noop();
+ let mut contexto = Context::from_waker(waker);
+ loop {
+ match futuro.as_mut().poll(&mut contexto) {
+ Poll::Ready(salida) => return salida,
+ Poll::Pending => {
+ if interrupts::are_enabled() {
+ x86_64::instructions::hlt();
+ } else {
+ core::hint::spin_loop();
+ }
+ }
+ }
+ }
+}
+
+/// Lee `buf.len() / 512` sectores consecutivos a partir de `sector`. Sincrono:
+/// construido sobre la maquinaria asincrona via `bloquear_en`. El bufer debe
+/// medir un multiplo entero de un sector.
+pub fn leer_sectores(sector: u64, buf: &mut [u8]) -> Result<(), &'static str> {
+ let datos = bloquear_en(leer_bloques(sector, buf.len() / TAM_SECTOR))?;
+ buf.copy_from_slice(&datos);
+ Ok(())
+}
+
+/// Escribe `buf.len() / 512` sectores consecutivos a partir de `sector`.
+/// Sincrono, sobre la misma maquinaria asincrona.
+pub fn escribir_sectores(sector: u64, buf: &[u8]) -> Result<(), &'static str> {
+ bloquear_en(escribir_bloques(sector, buf.to_vec())).map(|_| ())
+}
diff --git a/renaser/kernel/src/drivers/mod.rs b/renaser/kernel/src/drivers/mod.rs
new file mode 100644
index 0000000..91318fd
--- /dev/null
+++ b/renaser/kernel/src/drivers/mod.rs
@@ -0,0 +1,15 @@
+// =============================================================================
+// renaser :: kernel/src/drivers — Fase 6.1 :: el puente hacia el hardware real
+// -----------------------------------------------------------------------------
+// Hasta aqui renaser solo tocaba el silicio que el firmware le servia en
+// bandeja: el framebuffer GOP, el temporizador, el teclado. Los `drivers`
+// abren la primera via hacia hardware que el kernel debe DESCUBRIR y reclamar
+// por si mismo:
+//
+// * `pci` — acceso al espacio de configuracion del bus PCI (0xCF8/0xCFC).
+// * `disco` — el disco virtio-blk: asignador de marcos DMA, `Hal` y la
+// lectura, por sondeo, de su primer sector.
+// =============================================================================
+
+pub mod disco;
+pub mod pci;
diff --git a/renaser/kernel/src/drivers/pci.rs b/renaser/kernel/src/drivers/pci.rs
new file mode 100644
index 0000000..b1fd3f5
--- /dev/null
+++ b/renaser/kernel/src/drivers/pci.rs
@@ -0,0 +1,76 @@
+// =============================================================================
+// renaser :: kernel/src/drivers/pci.rs — Fase 6.1 :: el acceso al bus PCI
+// -----------------------------------------------------------------------------
+// El cargador `bootloader` entrega el mapa de memoria y el framebuffer, pero
+// NO un censo de perifericos: descubrir el disco es tarea del kernel. Aqui
+// renaser habla con el espacio de configuracion del bus PCI a traves del
+// mecanismo #1 —los puertos 0xCF8 (direccion) y 0xCFC (datos)—.
+//
+// Este modulo provee `CamPuertos`, una implementacion del rasgo
+// `ConfigurationAccess` de `virtio-drivers`: la crate enumera el bus y mapea
+// los BARs del dispositivo apoyandose en estas dos funciones de acceso.
+// =============================================================================
+
+use virtio_drivers::transport::pci::bus::{ConfigurationAccess, DeviceFunction};
+use x86_64::instructions::port::Port;
+
+/// Puerto de DIRECCION del mecanismo de configuracion PCI #1.
+const CONFIG_ADDRESS: u16 = 0xCF8;
+/// Puerto de DATOS del mecanismo de configuracion PCI #1.
+const CONFIG_DATA: u16 = 0xCFC;
+
+/// Compone la palabra de direccion del mecanismo #1: bit 31 de habilitacion,
+/// bus, dispositivo, funcion y offset de registro alineado a dword.
+fn direccion(device_function: DeviceFunction, offset: u8) -> u32 {
+ 0x8000_0000
+ | ((device_function.bus as u32) << 16)
+ | ((device_function.device as u32) << 11)
+ | ((device_function.function as u32) << 8)
+ | ((offset as u32) & 0xFC)
+}
+
+/// Lee un registro de 32 bits del espacio de configuracion PCI.
+fn leer(device_function: DeviceFunction, offset: u8) -> u32 {
+ // SEGURIDAD: 0xCF8/0xCFC son los puertos del mecanismo de configuracion
+ // PCI #1, fijos en la arquitectura PC. La direccion lleva su bit de
+ // habilitacion y el offset alineado a dword, como exige el protocolo.
+ unsafe {
+ Port::<u32>::new(CONFIG_ADDRESS).write(direccion(device_function, offset));
+ Port::<u32>::new(CONFIG_DATA).read()
+ }
+}
+
+/// Escribe un registro de 32 bits en el espacio de configuracion PCI.
+fn escribir(device_function: DeviceFunction, offset: u8, dato: u32) {
+ // SEGURIDAD: vease `leer` — mismos puertos, mismo protocolo del 8259/PCI.
+ unsafe {
+ Port::<u32>::new(CONFIG_ADDRESS).write(direccion(device_function, offset));
+ Port::<u32>::new(CONFIG_DATA).write(dato);
+ }
+}
+
+/// Lee el registro «Interrupt Line» (byte bajo del offset 0x3C): la linea de
+/// IRQ del PIC que el firmware UEFI enruto y asigno a este dispositivo. Es el
+/// puente entre el descubrimiento PCI y el manejo de interrupciones (Fase 6.2).
+pub fn linea_irq(device_function: DeviceFunction) -> u8 {
+ (leer(device_function, 0x3C) & 0xFF) as u8
+}
+
+/// Acceso al espacio de configuracion PCI por puertos de E/S — el mecanismo #1.
+/// Es un tipo sin estado: toda la informacion viaja en cada llamada.
+pub struct CamPuertos;
+
+impl ConfigurationAccess for CamPuertos {
+ fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 {
+ leer(device_function, register_offset)
+ }
+
+ fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) {
+ escribir(device_function, register_offset, data);
+ }
+
+ unsafe fn unsafe_clone(&self) -> Self {
+ // `CamPuertos` no tiene estado: clonarlo es trivial y sin riesgo.
+ CamPuertos
+ }
+}
diff --git a/renaser/kernel/src/gdt.rs b/renaser/kernel/src/gdt.rs
new file mode 100644
index 0000000..4c7831c
--- /dev/null
+++ b/renaser/kernel/src/gdt.rs
@@ -0,0 +1,80 @@
+// =============================================================================
+// renaser :: kernel/src/gdt.rs — Fase 2 :: cimientos del manejo de fallos
+// -----------------------------------------------------------------------------
+// La GDT y el TSS no se ven jamas, pero sostienen todo lo demas. Su cometido
+// esencial en renaser es reservar un stack de emergencia inquebrantable: si la
+// pila del kernel se desborda, el manejador de doble fallo aterrizara sobre
+// terreno firme en lugar de arrastrar al sistema hacia un fallo triple.
+//
+// Una sola verdad, instalada una sola vez, que PERSISTE bajo cada interrupcion.
+// =============================================================================
+
+use x86_64::instructions::segmentation::{Segment, CS, DS, ES, SS};
+use x86_64::instructions::tables::load_tss;
+use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};
+use x86_64::structures::tss::TaskStateSegment;
+use x86_64::VirtAddr;
+
+use crate::CeldaSync;
+
+/// Indice, dentro de la Interrupt Stack Table del TSS, del stack reservado
+/// para el manejador de doble fallo.
+pub const IST_DOBLE_FALLO: u16 = 0;
+
+/// Tamaño del stack de emergencia: 20 KiB. Holgura suficiente para diagnosticar
+/// el fallo y encender la baliza sin volver a tocar la pila comprometida.
+const TAM_STACK_EMERGENCIA: usize = 4096 * 5;
+
+/// Stack de emergencia del doble fallo. Vive en `.bss`; la IST apuntara a su
+/// extremo superior, pues la pila de x86_64 crece hacia direcciones menores.
+static STACK_EMERGENCIA: CeldaSync<[u8; TAM_STACK_EMERGENCIA]> =
+ CeldaSync::nueva([0; TAM_STACK_EMERGENCIA]);
+
+/// El Task State Segment. En 64 bits ya no describe «tareas»: lo conservamos
+/// unicamente por su Interrupt Stack Table.
+static TSS: CeldaSync<TaskStateSegment> = CeldaSync::nueva(TaskStateSegment::new());
+
+/// La Global Descriptor Table propia de renaser.
+static GDT: CeldaSync<GlobalDescriptorTable> = CeldaSync::nueva(GlobalDescriptorTable::new());
+
+/// Instala una GDT propia con su TSS y arma el stack de emergencia.
+///
+/// Debe invocarse una sola vez, durante el arranque, ANTES de cargar la IDT:
+/// la IDT inscribe en cada entrada el selector de codigo vigente en ese momento.
+pub fn init() {
+ // --- 1. Inscribir en el TSS la cima del stack de emergencia. ---
+ // La pila crece hacia abajo: la IST exige la direccion MAS ALTA del bloque.
+ let cima_stack = VirtAddr::from_ptr(STACK_EMERGENCIA.puntero()) + TAM_STACK_EMERGENCIA as u64;
+ // SEGURIDAD: `init` corre una sola vez, en arranque secuencial de un solo
+ // hilo; nada mas referencia el TSS todavia.
+ unsafe {
+ (*TSS.puntero()).interrupt_stack_table[IST_DOBLE_FALLO as usize] = cima_stack;
+ }
+
+ // --- 2. Construir la GDT: codigo de kernel, datos de kernel y el TSS. ---
+ // SEGURIDAD: mismo argumento de unicidad; la GDT aun no esta cargada y
+ // ningun otro flujo posee referencias a estas celdas.
+ let gdt: &'static mut GlobalDescriptorTable = unsafe { &mut *GDT.puntero() };
+ let tss: &'static TaskStateSegment = unsafe { &*TSS.puntero() };
+
+ let sel_codigo: SegmentSelector = gdt.append(Descriptor::kernel_code_segment());
+ let sel_datos: SegmentSelector = gdt.append(Descriptor::kernel_data_segment());
+ let sel_tss: SegmentSelector = gdt.append(Descriptor::tss_segment(tss));
+
+ // --- 3. Activar la GDT y recargar TODOS los registros de segmento. ---
+ let gdt_estatica: &'static GlobalDescriptorTable = gdt;
+ gdt_estatica.load();
+ // SEGURIDAD: los tres selectores apuntan a entradas validas de la GDT
+ // recien cargada. Recargar SS/DS/ES es IMPRESCINDIBLE, no opcional: el
+ // cargador nos deja esos registros con selectores de SU tabla; si SS
+ // conservara el valor heredado, el primer `iretq` de una rutina de
+ // excepcion intentaria recargar un selector que en nuestra GDT ya no
+ // describe un segmento de datos, y degeneraria en un #GP.
+ unsafe {
+ CS::set_reg(sel_codigo);
+ SS::set_reg(sel_datos);
+ DS::set_reg(sel_datos);
+ ES::set_reg(sel_datos);
+ load_tss(sel_tss);
+ }
+}
diff --git a/renaser/kernel/src/grafico.rs b/renaser/kernel/src/grafico.rs
new file mode 100644
index 0000000..721d12b
--- /dev/null
+++ b/renaser/kernel/src/grafico.rs
@@ -0,0 +1,325 @@
+// =============================================================================
+// renaser :: kernel/src/grafico.rs — el sustrato grafico del sistema
+// -----------------------------------------------------------------------------
+// En renaser el texto es un caso particular del dibujo, y el dibujo descansa
+// sobre este modulo: el color, el framebuffer fisico (`Pantalla`) y el lienzo
+// intermedio en RAM (`Lienzo`) que sostiene la tecnica de doble bufer.
+// =============================================================================
+
+use core::cell::UnsafeCell;
+use core::ptr;
+use core::sync::atomic::{AtomicBool, Ordering};
+
+use bootloader_api::info::{FrameBuffer, FrameBufferInfo, PixelFormat};
+use embedded_graphics::draw_target::DrawTarget;
+use embedded_graphics::geometry::{OriginDimensions, Size};
+use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
+use embedded_graphics::Pixel;
+
+/// Ancho maximo de lienzo soportado (Full HD).
+pub(crate) const ANCHO_MAX: usize = 1920;
+/// Alto maximo de lienzo soportado (Full HD).
+pub(crate) const ALTO_MAX: usize = 1080;
+/// Capacidad del lienzo intermedio, en pixeles de 32 bits.
+const PIXELES_MAX: usize = ANCHO_MAX * ALTO_MAX;
+
+// =============================================================================
+// COLOR — la unidad indivisible del lenguaje visual de renaser
+// =============================================================================
+
+/// Color de 24 bits, independiente del formato fisico del framebuffer.
+#[derive(Clone, Copy)]
+pub(crate) struct Color {
+ pub(crate) r: u8,
+ pub(crate) g: u8,
+ pub(crate) b: u8,
+}
+
+impl Color {
+ /// Reposo del lienzo: un indigo casi negro, sereno y persistente.
+ pub(crate) const LIENZO_EN_REPOSO: Color = Color {
+ r: 0x12,
+ g: 0x16,
+ b: 0x20,
+ };
+
+ /// Alerta de colapso: un rojo saturado, imposible de ignorar.
+ pub(crate) const ALERTA: Color = Color {
+ r: 0xD4,
+ g: 0x1E,
+ b: 0x2C,
+ };
+
+ /// Agotamiento de memoria (OOM): un naranja de advertencia.
+ pub(crate) const OOM: Color = Color {
+ r: 0xFF,
+ g: 0xA5,
+ b: 0x00,
+ };
+
+ /// Tinta del texto: un blanco suave, legible sobre el indigo.
+ pub(crate) const TEXTO: Color = Color {
+ r: 0xE8,
+ g: 0xEC,
+ b: 0xF4,
+ };
+
+ /// Desalojo de una aplicacion: un purpura inequivoco. Distinto del rojo de
+ /// colapso del kernel y del naranja de OOM — porque esto NO es un colapso:
+ /// es el kernel conteniendo a un inquilino discolo y siguiendo vivo.
+ pub(crate) const DESALOJO: Color = Color {
+ r: 0x8B,
+ g: 0x5C,
+ b: 0xF6,
+ };
+
+ /// Desalojo por desbordo de memoria: un amarillo palido. Distingue al
+ /// inquilino que revento su techo ESPACIAL del que agoto su tiempo (purpura).
+ pub(crate) const DESALOJO_MEMORIA: Color = Color {
+ r: 0xFF,
+ g: 0xFF,
+ b: 0xE0,
+ };
+}
+
+// =============================================================================
+// REGION — la sub-superficie que el kernel asigna a cada aplicacion
+// =============================================================================
+
+/// Una sub-region rectangular de la pantalla, en pixeles. El kernel asigna una
+/// a cada aplicacion del userspace: es, a la vez, su ventana al mundo y su
+/// confinamiento — una app jamas pinta un pixel fuera de la suya.
+#[derive(Clone, Copy)]
+pub(crate) struct RegionPantalla {
+ /// Desplazamiento horizontal de la esquina superior izquierda.
+ pub(crate) x: usize,
+ /// Desplazamiento vertical de la esquina superior izquierda.
+ pub(crate) y: usize,
+ /// Ancho de la region, en pixeles.
+ pub(crate) ancho: usize,
+ /// Alto de la region, en pixeles.
+ pub(crate) alto: usize,
+}
+
+impl RegionPantalla {
+ /// Numero total de pixeles que abarca la region.
+ pub(crate) const fn pixeles(&self) -> usize {
+ self.ancho * self.alto
+ }
+}
+
+/// Traduce un [`Color`] logico al valor nativo de 32 bits que el framebuffer
+/// espera, respetando el orden de canales que reporta el firmware UEFI.
+pub(crate) fn codificar(formato: PixelFormat, color: Color) -> u32 {
+ let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
+ match formato {
+ PixelFormat::Rgb => r | (g << 8) | (b << 16),
+ PixelFormat::Bgr => b | (g << 8) | (r << 16),
+ PixelFormat::U8 => (r * 54 + g * 183 + b * 19) >> 8,
+ PixelFormat::Unknown {
+ red_position,
+ green_position,
+ blue_position,
+ } => (r << red_position) | (g << green_position) | (b << blue_position),
+ _ => r | (g << 8) | (b << 16),
+ }
+}
+
+// =============================================================================
+// ESCRITURA VOLATIL — la unica celula que toca memoria de video real
+// =============================================================================
+
+/// Deposita un pixel ya codificado en una direccion del framebuffer fisico.
+/// Las escrituras son **volatiles**: el optimizador no puede elidirlas.
+///
+/// # Seguridad
+///
+/// `destino` debe apuntar a memoria de video valida y escribible para
+/// `bytes_por_pixel` bytes, correctamente alineada para el ancho de escritura.
+#[inline]
+pub(crate) unsafe fn escribir_pixel_volatil(destino: *mut u8, valor: u32, bytes_por_pixel: usize) {
+ match bytes_por_pixel {
+ 4 => unsafe { ptr::write_volatile(destino.cast::<u32>(), valor) },
+ 3 => unsafe {
+ ptr::write_volatile(destino, valor as u8);
+ ptr::write_volatile(destino.add(1), (valor >> 8) as u8);
+ ptr::write_volatile(destino.add(2), (valor >> 16) as u8);
+ },
+ 2 => unsafe { ptr::write_volatile(destino.cast::<u16>(), valor as u16) },
+ _ => unsafe { ptr::write_volatile(destino, valor as u8) },
+ }
+}
+
+// =============================================================================
+// LIENZO INTERMEDIO — el corazon del doble bufer
+// =============================================================================
+
+/// Respaldo estatico del lienzo, alineado a pagina. Vive en `.bss`.
+#[repr(align(4096))]
+struct LienzoEstatico(UnsafeCell<[u32; PIXELES_MAX]>);
+
+// SEGURIDAD: el acceso se serializa mediante `LIENZO_ENTREGADO`, que garantiza
+// un unico prestamo mutable durante toda la vida del sistema.
+unsafe impl Sync for LienzoEstatico {}
+
+/// Memoria de respaldo del lienzo intermedio.
+static MEMORIA_LIENZO: LienzoEstatico = LienzoEstatico(UnsafeCell::new([0u32; PIXELES_MAX]));
+
+/// Centinela de entrega unica del lienzo.
+static LIENZO_ENTREGADO: AtomicBool = AtomicBool::new(false);
+
+/// Entrega — exactamente una vez — el prestamo mutable de la memoria del lienzo.
+pub(crate) fn reclamar_memoria_lienzo() -> Option<&'static mut [u32]> {
+ if LIENZO_ENTREGADO.swap(true, Ordering::AcqRel) {
+ return None;
+ }
+ // SEGURIDAD: el `swap` anterior garantiza que este es el unico prestamo
+ // mutable de `MEMORIA_LIENZO` durante toda la ejecucion.
+ let arreglo: &'static mut [u32; PIXELES_MAX] = unsafe { &mut *MEMORIA_LIENZO.0.get() };
+ Some(arreglo.as_mut_slice())
+}
+
+/// Superficie de dibujo en RAM. Cada pixel se almacena ya codificado.
+pub(crate) struct Lienzo {
+ pub(crate) pixeles: &'static mut [u32],
+ pub(crate) ancho: usize,
+ pub(crate) alto: usize,
+ pub(crate) formato: PixelFormat,
+}
+
+impl Lienzo {
+ /// Construye un lienzo sobre la memoria de respaldo reclamada.
+ pub(crate) fn nuevo(
+ memoria: &'static mut [u32],
+ ancho: usize,
+ alto: usize,
+ formato: PixelFormat,
+ ) -> Lienzo {
+ Lienzo {
+ pixeles: memoria,
+ ancho,
+ alto,
+ formato,
+ }
+ }
+
+ /// Pinta un unico pixel. Las coordenadas fuera del lienzo se ignoran.
+ pub(crate) fn pintar_pixel(&mut self, x: usize, y: usize, color: Color) {
+ if x < self.ancho && y < self.alto {
+ self.pixeles[y * self.ancho + x] = codificar(self.formato, color);
+ }
+ }
+
+ /// Rellena un rectangulo, recortado con firmeza a los limites del lienzo.
+ pub(crate) fn rellenar_rect(
+ &mut self,
+ x0: usize,
+ y0: usize,
+ ancho: usize,
+ alto: usize,
+ color: Color,
+ ) {
+ let valor = codificar(self.formato, color);
+ let x_ini = x0.min(self.ancho);
+ let y_ini = y0.min(self.alto);
+ let x_fin = (x0 + ancho).min(self.ancho);
+ let y_fin = (y0 + alto).min(self.alto);
+
+ let mut y = y_ini;
+ while y < y_fin {
+ let base = y * self.ancho;
+ let mut x = x_ini;
+ while x < x_fin {
+ self.pixeles[base + x] = valor;
+ x += 1;
+ }
+ y += 1;
+ }
+ }
+
+ /// Inunda el lienzo entero con un color plano.
+ pub(crate) fn limpiar(&mut self, color: Color) {
+ self.rellenar_rect(0, 0, self.ancho, self.alto, color);
+ }
+}
+
+// `embedded-graphics` ve el lienzo como un `DrawTarget`: sus primitivas
+// vectoriales pueden dibujar directamente sobre el.
+impl DrawTarget for Lienzo {
+ type Color = Rgb888;
+ type Error = core::convert::Infallible;
+
+ fn draw_iter<I>(&mut self, pixeles: I) -> Result<(), Self::Error>
+ where
+ I: IntoIterator<Item = Pixel<Self::Color>>,
+ {
+ for Pixel(punto, color) in pixeles {
+ if let (Ok(x), Ok(y)) = (usize::try_from(punto.x), usize::try_from(punto.y)) {
+ self.pintar_pixel(
+ x,
+ y,
+ Color {
+ r: color.r(),
+ g: color.g(),
+ b: color.b(),
+ },
+ );
+ }
+ }
+ Ok(())
+ }
+}
+
+impl OriginDimensions for Lienzo {
+ fn size(&self) -> Size {
+ Size::new(self.ancho as u32, self.alto as u32)
+ }
+}
+
+// =============================================================================
+// PANTALLA — el framebuffer fisico GOP, envuelto en seguridad
+// =============================================================================
+
+/// Vista segura del framebuffer lineal entregado por el firmware UEFI.
+pub(crate) struct Pantalla {
+ pub(crate) base: *mut u8,
+ pub(crate) ancho: usize,
+ pub(crate) alto: usize,
+ pub(crate) paso_bytes: usize,
+ pub(crate) bytes_por_pixel: usize,
+}
+
+impl Pantalla {
+ /// Adopta el framebuffer descrito por `info`. La memoria de video es
+ /// permanente, asi que conservar su puntero crudo es legitimo.
+ pub(crate) fn adoptar(framebuffer: &mut FrameBuffer, info: FrameBufferInfo) -> Pantalla {
+ let base = framebuffer.buffer_mut().as_mut_ptr();
+ Pantalla {
+ base,
+ ancho: info.width,
+ alto: info.height,
+ paso_bytes: info.stride * info.bytes_per_pixel,
+ bytes_por_pixel: info.bytes_per_pixel,
+ }
+ }
+
+ /// Vuelca el lienzo intermedio sobre la pantalla fisica de un solo gesto.
+ pub(crate) fn presentar(&mut self, lienzo: &Lienzo) {
+ let ancho = self.ancho.min(lienzo.ancho);
+ let alto = self.alto.min(lienzo.alto);
+
+ for y in 0..alto {
+ let fila_fisica = y * self.paso_bytes;
+ let fila_lienzo = y * lienzo.ancho;
+ for x in 0..ancho {
+ let pixel = lienzo.pixeles[fila_lienzo + x];
+ // SEGURIDAD: x e y estan acotados por las dimensiones reales
+ // del framebuffer; el desplazamiento cae siempre dentro de el.
+ unsafe {
+ let destino = self.base.add(fila_fisica + x * self.bytes_por_pixel);
+ escribir_pixel_volatil(destino, pixel, self.bytes_por_pixel);
+ }
+ }
+ }
+ }
+}
diff --git a/renaser/kernel/src/interrupts.rs b/renaser/kernel/src/interrupts.rs
new file mode 100644
index 0000000..9d5e46a
--- /dev/null
+++ b/renaser/kernel/src/interrupts.rs
@@ -0,0 +1,148 @@
+// =============================================================================
+// renaser :: kernel/src/interrupts.rs — Fase 2/3 :: la IDT y los reflejos
+// -----------------------------------------------------------------------------
+// La Interrupt Descriptor Table es la tabla de reflejos del procesador. ante
+// cada excepcion o IRQ, la CPU abandona lo que hacia y salta a la entrada que
+// le corresponde. renaser distingue tres naturalezas de impulso:
+//
+// * RECUPERABLE — el breakpoint (#BP): se atiende y la ejecucion prosigue.
+// * FATAL — el resto de excepciones: se entregan al `panic!`.
+// * HARDWARE — temporizador, teclado y disco: ya NO escriben estado
+// rustico, sino que alimentan el reactor asincrono.
+// =============================================================================
+
+use core::sync::atomic::{AtomicU8, Ordering};
+
+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, PageFaultErrorCode};
+
+use crate::{gdt, pic, CeldaSync};
+
+/// La Interrupt Descriptor Table propia de renaser.
+static IDT: CeldaSync<InterruptDescriptorTable> =
+ CeldaSync::nueva(InterruptDescriptorTable::new());
+
+/// Vector de la IDT asignado a la IRQ del disco. Vale 0 hasta que el montaje
+/// del disco (Fase 6.2) descubra su linea de IRQ y la registre — ninguna IRQ
+/// legitima del disco vive en el vector 0 (reservado a las excepciones).
+static VECTOR_DISCO: AtomicU8 = AtomicU8::new(0);
+
+/// Construye y activa la Interrupt Descriptor Table.
+///
+/// Debe invocarse una sola vez, durante el arranque, DESPUES de [`gdt::init`].
+pub fn init() {
+ // SEGURIDAD: `init` corre una sola vez, en arranque secuencial de un solo
+ // hilo; nada mas referencia la IDT todavia.
+ let idt: &'static mut InterruptDescriptorTable = unsafe { &mut *IDT.puntero() };
+
+ // --- Excepciones de CPU ---
+ idt.breakpoint.set_handler_fn(reflejo_breakpoint);
+ idt.invalid_opcode.set_handler_fn(reflejo_opcode_invalido);
+ idt.divide_error.set_handler_fn(reflejo_division);
+ idt.general_protection_fault
+ .set_handler_fn(reflejo_proteccion_general);
+ idt.page_fault.set_handler_fn(reflejo_fallo_pagina);
+
+ // El doble fallo se atiende SIEMPRE sobre el stack de emergencia del TSS:
+ // ni un desbordamiento de la pila del kernel impedira su diagnostico.
+ // SEGURIDAD: el indice IST referido fue armado previamente por `gdt::init`.
+ unsafe {
+ idt.double_fault
+ .set_handler_fn(reflejo_doble_fallo)
+ .set_stack_index(gdt::IST_DOBLE_FALLO);
+ }
+
+ // --- Interrupciones de hardware, ya remapeadas por el PIC ---
+ idt[pic::VECTOR_TEMPORIZADOR].set_handler_fn(irq_temporizador);
+ idt[pic::VECTOR_TECLADO].set_handler_fn(irq_teclado);
+
+ let idt_estatica: &'static InterruptDescriptorTable = idt;
+ idt_estatica.load();
+}
+
+/// Registra el manejador de la IRQ del disco virtio-blk en la entrada de la IDT
+/// que corresponde a `irq` (Fase 6.2). Lo invoca el montaje del disco, una vez
+/// descubierta la linea de IRQ que el firmware le asigno.
+///
+/// Se llama durante el arranque secuencial, con las interrupciones aun
+/// desactivadas y la linea todavia enmascarada en el PIC: la entrada que se
+/// escribe no puede dispararse mientras se escribe.
+pub fn registrar_irq_disco(irq: u8) {
+ let vector = pic::vector_irq(irq);
+ VECTOR_DISCO.store(vector, Ordering::SeqCst);
+ // SEGURIDAD: el arranque es secuencial y de un solo hilo; las interrupciones
+ // estan desactivadas y la linea del disco, enmascarada. La IDT ya esta
+ // cargada (`init` hizo `lidt`), pero la CPU relee cada entrada en cada
+ // interrupcion: modificar esta entrada en memoria surte efecto de inmediato.
+ let idt: &'static mut InterruptDescriptorTable = unsafe { &mut *IDT.puntero() };
+ idt[vector].set_handler_fn(irq_disco);
+}
+
+// =============================================================================
+// REFLEJOS DE EXCEPCION — las rutinas a las que la CPU salta ante cada fallo
+// =============================================================================
+
+/// #BP — Breakpoint. Excepcion RECUPERABLE: se atiende sin ruido y, al
+/// retornar, la CPU reanuda la ejecucion justo tras la instruccion `int3`.
+extern "x86-interrupt" fn reflejo_breakpoint(_marco: InterruptStackFrame) {}
+
+/// #UD — Opcode invalido. Fatal: la corriente de instrucciones es incoherente.
+extern "x86-interrupt" fn reflejo_opcode_invalido(marco: InterruptStackFrame) {
+ panic!("EXCEPCION FATAL :: opcode invalido (#UD)\n{marco:#?}");
+}
+
+/// #DE — Error de division. Fatal.
+extern "x86-interrupt" fn reflejo_division(marco: InterruptStackFrame) {
+ panic!("EXCEPCION FATAL :: error de division (#DE)\n{marco:#?}");
+}
+
+/// #GP — Fallo de proteccion general. Fatal.
+extern "x86-interrupt" fn reflejo_proteccion_general(marco: InterruptStackFrame, codigo: u64) {
+ panic!("EXCEPCION FATAL :: proteccion general (#GP) codigo={codigo:#x}\n{marco:#?}");
+}
+
+/// #PF — Fallo de pagina. Fatal en esta fase (sin memoria virtual dinamica).
+extern "x86-interrupt" fn reflejo_fallo_pagina(
+ marco: InterruptStackFrame,
+ codigo: PageFaultErrorCode,
+) {
+ let direccion = x86_64::registers::control::Cr2::read_raw();
+ panic!("EXCEPCION FATAL :: fallo de pagina (#PF) en {direccion:#x} {codigo:?}\n{marco:#?}");
+}
+
+/// #DF — Doble fallo. Fatal e irreversible: por definicion, diverge. Se ejecuta
+/// sobre el stack de emergencia del TSS, nunca sobre la pila comprometida.
+extern "x86-interrupt" fn reflejo_doble_fallo(marco: InterruptStackFrame, _codigo: u64) -> ! {
+ panic!("EXCEPCION FATAL :: doble fallo (#DF)\n{marco:#?}");
+}
+
+// =============================================================================
+// IMPULSOS DE HARDWARE — productores de eventos para el reactor asincrono
+// =============================================================================
+
+/// IRQ0 — Temporizador. Desde la Fase 5 marca el compas del userspace: cada
+/// pulso avanza el `reloj` y despierta a las tareas que aguardaban su fotograma.
+extern "x86-interrupt" fn irq_temporizador(_marco: InterruptStackFrame) {
+ crate::async_system::reloj::pulso();
+ pic::fin_de_interrupcion(pic::VECTOR_TEMPORIZADOR);
+}
+
+/// IRQ1 — Teclado. Ya no escribe estado rustico: lee el scancode crudo y lo
+/// entrega al reactor asincrono, que despertara a la tarea del teclado.
+extern "x86-interrupt" fn irq_teclado(_marco: InterruptStackFrame) {
+ // SEGURIDAD: 0x60 es el puerto de datos del controlador PS/2, fijo en la
+ // arquitectura PC. Leerlo, ademas, libera al controlador para el siguiente.
+ let scancode: u8 = unsafe { x86_64::instructions::port::Port::new(0x60).read() };
+ crate::async_system::teclado::recibir_scancode(scancode);
+ pic::fin_de_interrupcion(pic::VECTOR_TECLADO);
+}
+
+/// IRQ del disco — virtio-blk (Fase 6.2). El disco ha terminado una
+/// transferencia: `atender_irq` reconoce la interrupcion en el dispositivo
+/// —lo que libera su linea— y despierta a la tarea que aguardaba el bloque.
+/// Asi la E/S de disco deja de bloquear el planificador por sondeo.
+extern "x86-interrupt" fn irq_disco(_marco: InterruptStackFrame) {
+ crate::drivers::disco::atender_irq();
+ // El EOI se cierra DESPUES de reconocer al dispositivo: una IRQ legada de
+ // PCI es de nivel — anunciar el fin sin haber bajado la linea la reavivaria.
+ pic::fin_de_interrupcion(VECTOR_DISCO.load(Ordering::SeqCst));
+}
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
new file mode 100644
index 0000000..1c24ec9
--- /dev/null
+++ b/renaser/kernel/src/main.rs
@@ -0,0 +1,342 @@
+// =============================================================================
+// renaser :: kernel/src/main.rs — el punto de entrada y la orquestacion
+// -----------------------------------------------------------------------------
+// Aqui no nace una terminal: nace una superficie. renaser es un kernel
+// asincrono de Espacio de Direccionamiento Unico (SASOS) que rompe con el
+// paradigma POSIX — sin TTYs, sin archivos planos, sin capas GNU.
+//
+// Este archivo es deliberadamente delgado: solo el arranque y el cableado.
+// Cada subsistema vive en su propio modulo (ver `ARCHITECTURE.md`):
+//
+// grafico — color, framebuffer fisico y lienzo de doble bufer.
+// consola — superficie de texto/imagen; rasteriza glifos con fontdue.
+// baliza — red de seguridad visual; manejadores de panico y de OOM.
+// sync — `CeldaSync`, la celda de inicializacion unica.
+// gdt — GDT + TSS + stack de emergencia del doble fallo.
+// interrupts — IDT, excepciones de CPU e interrupciones de hardware.
+// pic — el par 8259 remapeado y el temporizador (PIT).
+// drivers — descubrimiento de hardware y E/S de disco asincrona por
+// interrupcion: el bus PCI y el virtio-blk (Fases 6.1, 6.2).
+// almacen — el grafo de objetos direccionado por contenido (Fase 6.1c).
+// memory — el heap dinamico del kernel (`#[global_allocator]`).
+// async_system — el reactor cooperativo: ejecutor, tareas, wakers, teclado
+// y el reloj que marca el compas de los fotogramas (Fase 5).
+// texto — rasterizacion de tipografia vectorial (fontdue).
+// wasm — el runtime WebAssembly, la matriz de capacidades y el
+// escudo de combustible que acota el tiempo de cada app.
+// =============================================================================
+
+#![no_std] // Prohibido `std`: bajo nosotros no hay sistema operativo alguno.
+#![no_main] // El punto de entrada lo define el cargador, no la convencion C.
+#![feature(abi_x86_interrupt)] // ABI de las rutinas de excepcion (Fase 2).
+#![feature(alloc_error_handler)] // Manejador propio de agotamiento de heap (Fase 3).
+#![deny(unsafe_op_in_unsafe_fn)] // Cada operacion `unsafe` se justifica explicitamente.
+
+extern crate alloc; // El heap esta vivo: `alloc::*` queda disponible (Fase 3).
+
+use alloc::format;
+
+use bootloader_api::config::{BootloaderConfig, Mapping};
+use bootloader_api::info::{FrameBufferInfo, MemoryRegionKind, MemoryRegions, PixelFormat};
+use bootloader_api::{entry_point, BootInfo};
+use spin::Mutex;
+
+// --- Subsistemas del kernel ---
+mod almacen;
+mod async_system;
+mod baliza;
+mod consola;
+mod drivers;
+mod gdt;
+mod grafico;
+mod interrupts;
+mod memory;
+mod pic;
+mod sync;
+mod texto;
+mod wasm;
+
+// Reexportaciones para que los submodulos conserven rutas `crate::` estables.
+pub(crate) use consola::volcar_marco_wasm;
+pub(crate) use sync::CeldaSync;
+
+use async_system::executor::Executor;
+use baliza::BALIZA_PANICO;
+use consola::{Consola, CONSOLA};
+use grafico::{
+ codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX,
+ ANCHO_MAX,
+};
+
+/// El modulo WASM del userspace, empotrado en el binario del kernel para esta
+/// fase de pruebas. Es un `.wasm` puro, compilado aparte para `wasm32`. La
+/// Fase 5 lo instancia DOS veces —el mismo bytecode, dos regiones distintas—
+/// para demostrar la multitarea cooperativa sobre el espacio unico.
+static APP_WASM: &[u8] = include_bytes!("../assets/app.wasm");
+
+/// La aplicacion DISCOLA: un modulo WASM cuyo `tick` cae en un bucle cerrado y
+/// jamas retorna. Existe para una sola cosa — demostrar que el guardarrail de
+/// combustible la fulmina sin despeinar al kernel ni a sus vecinas.
+static DISCOLA_WASM: &[u8] = include_bytes!("../assets/discola.wasm");
+
+/// La aplicacion GLOTONA: un modulo WASM que reclama memoria lineal sin freno.
+/// Demuestra el guardarrail ESPACIAL — el techo de memoria la desaloja con la
+/// baliza amarilla, gemela de la purpura del desalojo por combustible.
+static GLOTONA_WASM: &[u8] = include_bytes!("../assets/glotona.wasm");
+
+/// La aplicacion CRONISTA: la primera ciudadana del userspace que escribe en el
+/// almacenamiento PERSISTENTE. En cada arranque graba un objeto en el grafo
+/// —enlazado al del arranque anterior—, lo corona como raiz y pinta una celda
+/// por cada arranque registrado. El disco recuerda; la cuenta sobrevive a los
+/// reinicios. Demuestra las capacidades `sys_object_*` de la Fase 6.1c.
+static CRONISTA_WASM: &[u8] = include_bytes!("../assets/cronista.wasm");
+
+/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU.
+static CONFIG_ARRANQUE: BootloaderConfig = {
+ let mut config = BootloaderConfig::new_default();
+ // Pedimos la memoria fisica mapeada: cimiento para futuras fases.
+ config.mappings.physical_memory = Some(Mapping::Dynamic);
+ config
+};
+
+// `entry_point!` genera el simbolo `_start`, valida la firma de `kernel_main`
+// y nos transfiere el control con seguridad de tipos.
+entry_point!(kernel_main, config = &CONFIG_ARRANQUE);
+
+/// Detiene la CPU de forma definitiva: `hlt` la duerme hasta una interrupcion.
+pub(crate) fn detener() -> ! {
+ loop {
+ x86_64::instructions::hlt();
+ }
+}
+
+/// Tarea cooperativa de una aplicacion WASM. En cada pulso del reloj le concede
+/// un `tick` —un fotograma de trabajo— y cede la CPU hasta el siguiente; entre
+/// medias corren sus vecinas. Si la app falla o agota su combustible, se la
+/// DESALOJA: se tatua su region con la baliza de purpura y la tarea concluye.
+/// El ejecutor la retira del censo, su memoria se libera, el kernel sigue vivo.
+async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
+ loop {
+ async_system::reloj::EsperaFrame::nueva().await;
+ if let Err(falla) = app.tick() {
+ // El color de la baliza delata la causa: purpura si agoto su tiempo
+ // o aborto, amarillo si reviento su techo de memoria.
+ consola::pintar_desalojo(app.region(), falla.color_baliza());
+ return;
+ }
+ }
+}
+
+/// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
+/// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las
+/// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el
+/// bloque esta listo. Deja en la consola el resultado y cuantas interrupciones
+/// de disco se atendieron — el testigo de que la E/S por sondeo quedo atras.
+async fn tarea_sonda_disco() {
+ let resultado = drivers::disco::leer_bloques(0, 1).await;
+ let Some(consola) = CONSOLA.get() else {
+ return;
+ };
+ let mut consola = consola.lock();
+ match resultado {
+ Ok(_) => consola.escribir(&format!(
+ "disco :: sonda asincrona OK -- {} IRQ de disco atendidas\n",
+ drivers::disco::pulsos_disco(),
+ )),
+ Err(motivo) => {
+ consola.escribir(&format!("disco :: sonda asincrona fallida -- {motivo}\n"))
+ }
+ }
+ consola.presentar();
+}
+
+/// Da vida a una aplicacion del userspace: la carga en su region y, si lo
+/// logra, la despacha como tarea cooperativa del reactor. Una carga fallida se
+/// salda pintando su region con la baliza de desalojo — el kernel no se inmuta.
+fn encender_app(ejecutor: &mut Executor, bytecode: &'static [u8], region: RegionPantalla) {
+ match wasm::AplicacionWasm::cargar(bytecode, region) {
+ Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
+ Err(_) => consola::pintar_desalojo(region, Color::DESALOJO),
+ }
+}
+
+/// Localiza la mayor region de RAM libre que el cargador reporto — la cantera
+/// de la que el DMA del disco tomara sus marcos fisicos.
+fn mayor_region_usable(regiones: &MemoryRegions) -> Option<(u64, u64)> {
+ regiones
+ .iter()
+ .filter(|region| matches!(region.kind, MemoryRegionKind::Usable))
+ .map(|region| (region.start, region.end))
+ .max_by_key(|&(inicio, fin)| fin - inicio)
+}
+
+/// FASE 6.1c — funda el grafo de objetos. Monta el disco virtio-blk, lee o
+/// forja el superbloque, reconstruye el indice recorriendo el log y deja
+/// constancia visual: cuantos sectores tiene el disco, cuantos objetos viven
+/// ya en el grafo y si el arranque encontro —o no— una raiz de la que tirar.
+fn informar_almacen() {
+ // Fundar el almacen ANTES de tomar el cerrojo de la consola: el montaje
+ // del disco hace E/S por sondeo y nada de ello reclama la consola.
+ let resultado = almacen::init();
+ let Some(consola) = CONSOLA.get() else {
+ return;
+ };
+ let mut consola = consola.lock();
+ match resultado {
+ Ok(resumen) => {
+ let estado = if resumen.formateado {
+ "disco formateado"
+ } else {
+ "grafo montado"
+ };
+ consola.escribir(&format!(
+ "almacen :: {} :: {} sectores :: {} objetos :: raiz {}\n",
+ estado,
+ resumen.capacidad,
+ resumen.objetos,
+ if resumen.raiz { "presente" } else { "ausente" },
+ ));
+ }
+ Err(motivo) => {
+ consola.escribir(&format!("almacen :: fallo :: {motivo}\n"));
+ }
+ }
+ // FASE 6.2 — dejar constancia de la linea de IRQ por la que el disco
+ // anunciara, ya sin sondeo, el fin de cada transferencia.
+ match drivers::disco::irq() {
+ Some(irq) => {
+ consola.escribir(&format!("disco :: virtio-blk en IRQ {irq} -- E/S asincrona\n"))
+ }
+ None => consola.escribir("disco :: IRQ no enrutada -- E/S por sondeo\n"),
+ }
+ consola.presentar();
+}
+
+// =============================================================================
+// PUNTO DE ENTRADA DEL KERNEL
+// =============================================================================
+
+fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
+ // --- 1. Recuperar el framebuffer GOP que el firmware nos confio. ---
+ let framebuffer = match boot_info.framebuffer.as_mut() {
+ Some(fb) => fb,
+ None => detener(),
+ };
+ let info: FrameBufferInfo = framebuffer.info();
+ let formato: PixelFormat = info.pixel_format;
+ let pantalla = Pantalla::adoptar(framebuffer, info);
+
+ // Datos para la sonda de disco (Fase 6.1b): el offset al que el cargador
+ // mapeo la memoria fisica y la mayor region de RAM libre para el DMA.
+ let offset_fisico = boot_info.physical_memory_offset.into_option();
+ let region_dma = mayor_region_usable(&boot_info.memory_regions);
+
+ // --- 2. Encender la baliza: la red de seguridad visual va primero. ---
+ BALIZA_PANICO.encender(
+ &pantalla,
+ codificar(formato, Color::ALERTA),
+ codificar(formato, Color::OOM),
+ );
+
+ // --- 3. Cimientos de fallos e interrupciones (Fases 2.0 y 2.1). ---
+ gdt::init();
+ interrupts::init();
+ pic::init();
+
+ // --- 4. FASE 3 :: fundar el heap. A partir de aqui, `alloc` esta vivo. ---
+ memory::init();
+
+ // --- 5. Con el heap activo, fundar lo que depende de el: el canal de
+ // scancodes, el reloj de fotogramas y la tipografia vectorial. ---
+ async_system::teclado::init();
+ async_system::reloj::init();
+ texto::init();
+
+ // --- 6. Construir el lienzo y la consola; pintar el rotulo inicial,
+ // ya rasterizado por fontdue, y publicar la consola globalmente. ---
+ let memoria = match reclamar_memoria_lienzo() {
+ Some(m) => m,
+ None => detener(),
+ };
+ let mut lienzo = Lienzo::nuevo(
+ memoria,
+ info.width.min(ANCHO_MAX),
+ info.height.min(ALTO_MAX),
+ formato,
+ );
+ lienzo.limpiar(Color::LIENZO_EN_REPOSO);
+
+ let mut consola = Consola::nueva(lienzo, pantalla);
+ consola.escribir("renaser :: fase 6.2 -- E/S de disco asincrona por interrupcion\n");
+ consola.presentar();
+ CONSOLA.call_once(|| Mutex::new(consola));
+
+ // --- 6.5. FASE 6.1c :: fundar el subsistema de disco y, sobre el, el grafo
+ // de objetos: enumerar el bus PCI, montar el transporte virtio-blk,
+ // y leer o forjar el superbloque del almacen direccionado por
+ // contenido. El kernel adquiere, por fin, una memoria que perdura. ---
+ match (offset_fisico, region_dma) {
+ (Some(offset), Some((inicio, fin))) => {
+ drivers::disco::init(offset, inicio, fin);
+ informar_almacen();
+ }
+ _ => {
+ if let Some(consola) = CONSOLA.get() {
+ let mut consola = consola.lock();
+ consola.escribir("virtio-blk :: omitido -- memoria fisica sin mapear\n");
+ consola.presentar();
+ }
+ }
+ }
+
+ // --- 7. FASE 5/6 :: levantar el reactor y poblar el userspace con CINCO
+ // aplicaciones WASM concurrentes, cada una en su propia region:
+ //
+ // * App 1 — instancia de hello_wasm, a la izquierda, gobernada
+ // por el teclado.
+ // * App 2 — segunda instancia del MISMO bytecode, a la derecha:
+ // un unico modulo en la RAM unificada, dos vidas aisladas.
+ // * App discola — su `tick` es un bucle cerrado: el escudo de
+ // COMBUSTIBLE la desaloja (baliza purpura) en su 1er fotograma.
+ // * App glotona — reclama memoria sin freno: el escudo ESPACIAL
+ // la desaloja (baliza amarilla) en su 1er fotograma.
+ // * App cronista — escribe en el GRAFO DE OBJETOS persistente:
+ // cada arranque deja un objeto enlazado al anterior y pinta una
+ // celda por arranque. El disco recuerda entre reinicios.
+ //
+ // Las interrupciones se habilitan AHORA: el temporizador marcara el
+ // compas de los fotogramas y la IRQ del teclado difundira cada
+ // scancode a los canales que las apps consultan. ---
+ let mut ejecutor = Executor::nuevo();
+ encender_app(
+ &mut ejecutor,
+ APP_WASM,
+ RegionPantalla { x: 100, y: 120, ancho: 480, alto: 560 },
+ );
+ encender_app(
+ &mut ejecutor,
+ APP_WASM,
+ RegionPantalla { x: 700, y: 120, ancho: 480, alto: 560 },
+ );
+ encender_app(
+ &mut ejecutor,
+ DISCOLA_WASM,
+ RegionPantalla { x: 60, y: 700, ancho: 360, alto: 80 },
+ );
+ encender_app(
+ &mut ejecutor,
+ GLOTONA_WASM,
+ RegionPantalla { x: 460, y: 700, ancho: 360, alto: 80 },
+ );
+ encender_app(
+ &mut ejecutor,
+ CRONISTA_WASM,
+ RegionPantalla { x: 860, y: 700, ancho: 360, alto: 80 },
+ );
+ // FASE 6.2 :: una tarea mas del reactor — no una app WASM— que sondea el
+ // disco de forma ASINCRONA: la demostracion de que la IRQ del disco
+ // conduce la E/S sin detener a las aplicaciones visuales.
+ ejecutor.spawn(tarea_sonda_disco());
+ x86_64::instructions::interrupts::enable();
+ ejecutor.run();
+}
diff --git a/renaser/kernel/src/memory/allocator.rs b/renaser/kernel/src/memory/allocator.rs
new file mode 100644
index 0000000..4164c04
--- /dev/null
+++ b/renaser/kernel/src/memory/allocator.rs
@@ -0,0 +1,42 @@
+// =============================================================================
+// renaser :: kernel/src/memory/allocator.rs — Fase 3 :: el asignador global
+// -----------------------------------------------------------------------------
+// Reutilizamos un algoritmo probado (`linked_list_allocator`) en lugar de
+// arriesgar un asignador propio a bugs de fragmentacion. El heap es una region
+// estatica que PERSISTE durante toda la vida del kernel.
+// =============================================================================
+
+use linked_list_allocator::LockedHeap;
+
+use crate::CeldaSync;
+
+/// 64 MiB de heap para el kernel. La arquitectura estimaba 16, pero `fontdue`,
+/// al analizar una tipografia real, exige mas holgura; el manejador de OOM —la
+/// franja naranja— fue justamente lo que delato esa cota demasiado corta.
+const TAM_HEAP: usize = 64 * 1024 * 1024;
+
+/// Region de respaldo del heap, alineada a pagina, residente en `.bss`. Su
+/// campo solo se alcanza via puntero crudo —asi lo exige `GlobalAlloc`—, de ahi
+/// el `allow(dead_code)`: la memoria se usa, aunque no por una via «normal».
+#[repr(align(4096))]
+#[allow(dead_code)]
+struct RegionHeap([u8; TAM_HEAP]);
+
+/// La memoria fisica del heap. Nace a ceros, no engorda el binario.
+static REGION_HEAP: CeldaSync<RegionHeap> = CeldaSync::nueva(RegionHeap([0u8; TAM_HEAP]));
+
+/// El asignador global. Todo `alloc::*` —`Box`, `Vec`, `BTreeMap`, `Arc`...—
+/// se apoya, en silencio, sobre este.
+#[global_allocator]
+static ASIGNADOR: LockedHeap = LockedHeap::empty();
+
+/// Funda el heap del kernel. Debe invocarse UNA sola vez, en el arranque,
+/// antes del primer uso de cualquier estructura de `alloc`.
+pub fn init() {
+ let inicio: *mut u8 = REGION_HEAP.puntero().cast::<u8>();
+ // SEGURIDAD: la region es estatica, de uso exclusivo del asignador, vive
+ // tanto como el kernel, y `init` se invoca una unica vez en el arranque.
+ unsafe {
+ ASIGNADOR.lock().init(inicio, TAM_HEAP);
+ }
+}
diff --git a/renaser/kernel/src/memory/mod.rs b/renaser/kernel/src/memory/mod.rs
new file mode 100644
index 0000000..c858b3f
--- /dev/null
+++ b/renaser/kernel/src/memory/mod.rs
@@ -0,0 +1,11 @@
+// =============================================================================
+// renaser :: kernel/src/memory — la memoria dinamica del kernel (Fase 3)
+// -----------------------------------------------------------------------------
+// El heap no es, en renaser, una utilidad pasiva al estilo C. Existe para
+// sostener algo vivo: la cola de futuros y los reactores asincronos sobre los
+// que, fase a fase, se ejecutara el bytecode WASM aislado por software.
+// =============================================================================
+
+pub mod allocator;
+
+pub use allocator::init;
diff --git a/renaser/kernel/src/pic.rs b/renaser/kernel/src/pic.rs
new file mode 100644
index 0000000..7e1ff03
--- /dev/null
+++ b/renaser/kernel/src/pic.rs
@@ -0,0 +1,147 @@
+// =============================================================================
+// renaser :: kernel/src/pic.rs — Fase 2.1 :: el par 8259 y el metronomo
+// -----------------------------------------------------------------------------
+// El controlador de interrupciones 8259 (PIC) nace, por herencia historica,
+// con sus vectores solapados sobre los de las excepciones de CPU. Antes de
+// encender las interrupciones HAY que remapearlo: desplazar sus vectores
+// fuera del rango 0..31. Aqui tambien programamos el PIT, el temporizador
+// que dara a renaser su latido regular — el origen de su fluidez.
+// =============================================================================
+
+use x86_64::instructions::port::Port;
+
+// --- Puertos del par de 8259 (maestro + esclavo en cascada). ---
+const CMD_MAESTRO: u16 = 0x20;
+const DATOS_MAESTRO: u16 = 0x21;
+const CMD_ESCLAVO: u16 = 0xA0;
+const DATOS_ESCLAVO: u16 = 0xA1;
+
+// --- Puertos del temporizador de intervalos programable (PIT). ---
+const PIT_COMANDO: u16 = 0x43;
+const PIT_CANAL0: u16 = 0x40;
+/// Frecuencia base del cristal del PIT, en Hz.
+const PIT_BASE_HZ: u32 = 1_193_182;
+
+/// Orden de «fin de interrupcion» que el PIC espera tras cada IRQ atendida.
+const EOI: u8 = 0x20;
+
+/// Vector base del PIC maestro tras el remapeo (justo encima de las 32
+/// excepciones reservadas de la arquitectura).
+const OFFSET_MAESTRO: u8 = 0x20;
+/// Vector base del PIC esclavo tras el remapeo.
+const OFFSET_ESCLAVO: u8 = 0x28;
+
+/// Vector de la IRQ0 — el temporizador (PIT).
+pub const VECTOR_TEMPORIZADOR: u8 = OFFSET_MAESTRO; // 0x20
+/// Vector de la IRQ1 — el teclado.
+pub const VECTOR_TECLADO: u8 = OFFSET_MAESTRO + 1; // 0x21
+
+/// Remapea el par 8259 y programa el PIT a 100 Hz.
+///
+/// Debe invocarse una sola vez, tras cargar la IDT y ANTES de habilitar las
+/// interrupciones con `sti`.
+pub fn init() {
+ remapear();
+ configurar_temporizador(100);
+}
+
+/// Reprograma los vectores del 8259 fuera del rango de las excepciones de CPU.
+fn remapear() {
+ // SEGURIDAD: estos son los puertos de E/S estandar del 8259 en la
+ // arquitectura PC; la secuencia ICW1..ICW4 es su protocolo de inicio.
+ unsafe {
+ let mut cmd_m = Port::<u8>::new(CMD_MAESTRO);
+ let mut dat_m = Port::<u8>::new(DATOS_MAESTRO);
+ let mut cmd_e = Port::<u8>::new(CMD_ESCLAVO);
+ let mut dat_e = Port::<u8>::new(DATOS_ESCLAVO);
+ // Escribir en un puerto inerte da al 8259 el respiro que necesita
+ // entre palabras de control en hardware antiguo.
+ let mut respiro = Port::<u8>::new(0x80);
+
+ // ICW1 — iniciar la secuencia: en cascada, con ICW4 presente.
+ cmd_m.write(0x11u8);
+ respiro.write(0u8);
+ cmd_e.write(0x11u8);
+ respiro.write(0u8);
+ // ICW2 — el remapeo en si: desplazar los vectores lejos de 0..31.
+ dat_m.write(OFFSET_MAESTRO);
+ respiro.write(0u8);
+ dat_e.write(OFFSET_ESCLAVO);
+ respiro.write(0u8);
+ // ICW3 — declarar el cableado de la cascada (el esclavo en la IRQ2).
+ dat_m.write(0b0000_0100u8);
+ respiro.write(0u8);
+ dat_e.write(0b0000_0010u8);
+ respiro.write(0u8);
+ // ICW4 — modo 8086.
+ dat_m.write(0x01u8);
+ respiro.write(0u8);
+ dat_e.write(0x01u8);
+ respiro.write(0u8);
+ // Mascaras: el maestro deja pasar la IRQ0 (temporizador) y la IRQ1
+ // (teclado); todo lo demas, en silencio hasta que renaser lo reclame.
+ dat_m.write(0b1111_1100u8);
+ dat_e.write(0b1111_1111u8);
+ }
+}
+
+/// Programa el canal 0 del PIT para que emita la IRQ0 a `frecuencia_hz`.
+fn configurar_temporizador(frecuencia_hz: u32) {
+ let divisor = (PIT_BASE_HZ / frecuencia_hz) as u16;
+ // SEGURIDAD: 0x43 y 0x40 son los puertos estandar del PIT en el PC.
+ unsafe {
+ let mut comando = Port::<u8>::new(PIT_COMANDO);
+ let mut canal0 = Port::<u8>::new(PIT_CANAL0);
+ // Canal 0, acceso lobyte+hibyte, modo 3 (generador de onda cuadrada).
+ comando.write(0x36u8);
+ canal0.write((divisor & 0xFF) as u8);
+ canal0.write((divisor >> 8) as u8);
+ }
+}
+
+/// Notifica al PIC el «fin de interrupcion» de la IRQ recien atendida. Sin
+/// este aviso, el PIC jamas volveria a emitir esa interrupcion.
+pub fn fin_de_interrupcion(vector: u8) {
+ // SEGURIDAD: enviar EOI al puerto de comandos del PIC tras atender su IRQ
+ // es el cierre obligatorio del protocolo del 8259.
+ unsafe {
+ // Si la IRQ provino del esclavo, ambos PIC deben recibir el EOI.
+ if vector >= OFFSET_ESCLAVO {
+ Port::<u8>::new(CMD_ESCLAVO).write(EOI);
+ }
+ Port::<u8>::new(CMD_MAESTRO).write(EOI);
+ }
+}
+
+/// Vector de la IDT que corresponde a una linea de IRQ (0..15). El remapeo dejo
+/// las 16 lineas del par 8259 en vectores contiguos desde `OFFSET_MAESTRO`: la
+/// IRQ8 cae en `OFFSET_ESCLAVO`, que es justo `OFFSET_MAESTRO + 8`.
+pub fn vector_irq(irq: u8) -> u8 {
+ OFFSET_MAESTRO + irq
+}
+
+/// Levanta la mascara de una linea de IRQ — el PIC empezara a emitirla. Si la
+/// linea vive en el PIC esclavo (8..15), abre tambien la cascada del maestro
+/// (la IRQ2), sin la cual el esclavo jamas alcanzaria a la CPU.
+///
+/// Debe invocarse en el arranque, antes de habilitar las interrupciones.
+pub fn desenmascarar(irq: u8) {
+ // SEGURIDAD: 0x21 y 0xA1 son los puertos de mascara del par 8259 en la
+ // arquitectura PC; leer-modificar-escribir es la via de tocar una linea
+ // sola sin perturbar a las demas.
+ unsafe {
+ if irq < 8 {
+ let mut datos = Port::<u8>::new(DATOS_MAESTRO);
+ let mascara = datos.read();
+ datos.write(mascara & !(1 << irq));
+ } else {
+ let mut datos_e = Port::<u8>::new(DATOS_ESCLAVO);
+ let mascara_e = datos_e.read();
+ datos_e.write(mascara_e & !(1 << (irq - 8)));
+ // La cascada: el esclavo entrega sus IRQ al maestro por la IRQ2.
+ let mut datos_m = Port::<u8>::new(DATOS_MAESTRO);
+ let mascara_m = datos_m.read();
+ datos_m.write(mascara_m & !(1 << 2));
+ }
+ }
+}
diff --git a/renaser/kernel/src/sync.rs b/renaser/kernel/src/sync.rs
new file mode 100644
index 0000000..5af3f1c
--- /dev/null
+++ b/renaser/kernel/src/sync.rs
@@ -0,0 +1,29 @@
+// =============================================================================
+// renaser :: kernel/src/sync.rs — la celda de inicializacion unica
+// -----------------------------------------------------------------------------
+// Las estructuras globales del kernel (GDT, TSS, IDT, el heap...) nacen una
+// sola vez, durante el arranque secuencial y de un solo hilo, y despues solo
+// se leen. `CeldaSync` envuelve ese unico `unsafe` en una abstraccion comun:
+// un contrato de unicidad que el codigo de arranque garantiza por construccion.
+// =============================================================================
+
+use core::cell::UnsafeCell;
+
+/// Celda `Sync` para estado global de inicializacion unica.
+pub(crate) struct CeldaSync<T>(UnsafeCell<T>);
+
+// SEGURIDAD: cada celda se escribe una sola vez, durante el arranque, antes de
+// que existan interrupciones o concurrencia; despues es de solo lectura.
+unsafe impl<T> Sync for CeldaSync<T> {}
+
+impl<T> CeldaSync<T> {
+ /// Crea una celda con su valor inicial.
+ pub(crate) const fn nueva(valor: T) -> Self {
+ CeldaSync(UnsafeCell::new(valor))
+ }
+
+ /// Puntero crudo al contenido. Quien lo usa asume el contrato de unicidad.
+ pub(crate) fn puntero(&self) -> *mut T {
+ self.0.get()
+ }
+}
diff --git a/renaser/kernel/src/texto.rs b/renaser/kernel/src/texto.rs
new file mode 100644
index 0000000..9f71bea
--- /dev/null
+++ b/renaser/kernel/src/texto.rs
@@ -0,0 +1,36 @@
+// =============================================================================
+// renaser :: kernel/src/texto.rs — Fase 3 :: el texto como caso del dibujo
+// -----------------------------------------------------------------------------
+// Con el heap activo, el texto deja de ser un mapa de bits estatico. Una
+// tipografia vectorial se empotra en el binario y se rasteriza glifo a glifo,
+// bajo demanda: el texto se vuelve, literalmente, un caso particular del
+// dibujo — la promesa fundacional de renaser.
+// =============================================================================
+
+use alloc::vec::Vec;
+
+use fontdue::{Font, FontSettings, Metrics};
+use spin::Once;
+
+/// La tipografia vectorial, empotrada en el propio binario del kernel.
+static FUENTE_TTF: &[u8] = include_bytes!("../assets/font.ttf");
+
+/// La fuente ya analizada. Se funde una sola vez, tras activar el heap.
+static FUENTE: Once<Font> = Once::new();
+
+/// Analiza la tipografia empotrada. Requiere el heap ya activo.
+pub fn init() {
+ FUENTE.call_once(|| {
+ Font::from_bytes(FUENTE_TTF, FontSettings::default())
+ .expect("renaser :: la tipografia empotrada es invalida")
+ });
+}
+
+/// Rasteriza un glifo al vuelo: devuelve sus metricas de colocacion y un mapa
+/// de cobertura (un byte de opacidad, 0..=255, por cada pixel del glifo).
+pub fn rasterizar(caracter: char, tam_px: f32) -> (Metrics, Vec<u8>) {
+ FUENTE
+ .get()
+ .expect("renaser :: la tipografia no fue fundida")
+ .rasterize(caracter, tam_px)
+}
diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs
new file mode 100644
index 0000000..bbed9d1
--- /dev/null
+++ b/renaser/kernel/src/wasm/env.rs
@@ -0,0 +1,349 @@
+// =============================================================================
+// renaser :: kernel/src/wasm/env.rs — Fase 4/5/6 :: la matriz de capacidades
+// -----------------------------------------------------------------------------
+// El aislamiento de renaser no descansa en `int 0x80` ni en `sysenter`: no hay
+// vectores de syscall. Una aplicacion WASM solo puede hacer aquello para lo
+// que el kernel le haya inyectado una FUNCION DEL HOST. Esta matriz concede:
+//
+// * sys_render_frame — componer un fotograma en su region de pantalla;
+// * sys_get_scancode — consultar su canal de teclado;
+// * sys_object_put — grabar un objeto en el grafo (Fase 6.1c);
+// * sys_object_datos — leer la carga util de un objeto;
+// * sys_object_hijo — recorrer las aristas del DAG;
+// * sys_object_raiz — leer la raiz del grafo;
+// * sys_object_fijar_raiz — coronar un objeto como raiz.
+//
+// GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le
+// entrega contra los limites reales de su memoria lineal. No se confia en que
+// el runtime lo haga; se verifica aqui, antes de leer o escribir un solo byte.
+//
+// DOS CLASES DE FALLO. Un puntero fuera de limites es CULPA DE LA APP: se
+// devuelve un `Error` que la ABORTA (el kernel la captura y la desaloja). Un
+// fallo del almacenamiento —disco, objeto inexistente— NO es culpa de la app:
+// se le devuelve un codigo de error negativo, y la app decide que hacer.
+// =============================================================================
+
+use wasmi::{Caller, Error, Extern, Linker, Memory, StoreLimits};
+
+use crate::almacen::Hash;
+use crate::async_system::teclado::CanalTeclado;
+use crate::grafico::RegionPantalla;
+
+/// El estado del host adscrito al `Store` de una aplicacion: cuanto necesita
+/// una capacidad para servir a ESA app y a ninguna otra — su region de pantalla,
+/// su canal de teclado y sus cuotas de recursos. Dos apps jamas comparten nada.
+pub(crate) struct ContextoCapacidades {
+ /// La sub-region de pantalla asignada a la aplicacion.
+ pub(crate) region: RegionPantalla,
+ /// El canal de teclado propio de la aplicacion.
+ pub(crate) canal: CanalTeclado,
+ /// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
+ /// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
+ pub(crate) limites: StoreLimits,
+}
+
+/// Recupera la memoria lineal exportada por el modulo. Que no la exporte es un
+/// modulo mal formado: se aborta.
+fn obtener_memoria(caller: &Caller<'_, ContextoCapacidades>) -> Result<Memory, Error> {
+ caller
+ .get_export("memory")
+ .and_then(Extern::into_memory)
+ .ok_or_else(|| Error::new("WASM :: el modulo no exporta su memoria lineal"))
+}
+
+/// VALIDACION INFRANQUEABLE DE LIMITES. Comprueba que `[ptr, ptr + len)` cae
+/// entera dentro de la memoria lineal `m` y devuelve ese sub-slice. Un rango
+/// que se desborde aborta la app — el `Error` se traduce en una trampa de WASM.
+fn rango<'a>(m: &'a [u8], ptr: u32, len: usize, fallo: &'static str) -> Result<&'a [u8], Error> {
+ let inicio = ptr as usize;
+ match inicio.checked_add(len) {
+ Some(fin) if fin <= m.len() => Ok(&m[inicio..fin]),
+ _ => Err(Error::new(fallo)),
+ }
+}
+
+/// Lee un hash de 32 bytes de la memoria lineal, con sus limites verificados.
+fn leer_hash(m: &[u8], ptr: u32, fallo: &'static str) -> Result<Hash, Error> {
+ let bytes = rango(m, ptr, 32, fallo)?;
+ let mut hash = [0u8; 32];
+ hash.copy_from_slice(bytes);
+ Ok(hash)
+}
+
+/// Inyecta en el enlazador la matriz de capacidades del modulo WASM. Todo lo
+/// que no se defina aqui le queda, al modulo, fisicamente fuera de alcance.
+///
+/// Devuelve `Err` si una capacidad no se pudo enlazar — un fallo del kernel,
+/// no de la app; aun asi se propaga como `Result` para no incendiar nada.
+pub(crate) fn enlazar_capacidades(
+ enlazador: &mut Linker<ContextoCapacidades>,
+) -> Result<(), Error> {
+ // --- CAPACIDAD 1 :: sys_render_frame(ptr, len) ---
+ // El modulo entrega (ptr, len) hacia su PROPIA memoria lineal; el kernel
+ // valida esos limites y, solo entonces, compone el fotograma DENTRO de la
+ // region asignada a la app.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_render_frame",
+ |caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> {
+ let region = caller.data().region;
+
+ // El fotograma debe medir EXACTAMENTE los pixeles de la region. Un
+ // tamaño distinto delata a una app que pinta fuera de su ventana:
+ // se aborta antes de tocar un byte.
+ let esperado = region.pixeles() * 4;
+ if len as usize != esperado {
+ return Err(Error::new(
+ "WASM :: sys_render_frame con un fotograma ajeno a la region asignada",
+ ));
+ }
+
+ let memoria = obtener_memoria(&caller)?;
+ let datos: &[u8] = memoria.data(&caller);
+
+ // VALIDACION INFRANQUEABLE: si (ptr, len) se sale de la memoria
+ // lineal del modulo, se aborta la app —no el kernel—.
+ let marco = rango(
+ datos,
+ ptr,
+ len as usize,
+ "WASM :: sys_render_frame desbordo la memoria lineal del modulo",
+ )?;
+
+ // Limites verificados: la region es segura de leer y componer.
+ crate::volcar_marco_wasm(region, marco);
+ Ok(())
+ },
+ )?;
+
+ // --- CAPACIDAD 2 :: sys_get_scancode() -> u32 ---
+ // Expone, sin bloquear, el siguiente scancode del canal PROPIO de la app.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_get_scancode",
+ |caller: Caller<'_, ContextoCapacidades>| -> u32 {
+ caller.data().canal.pop().unwrap_or(0) as u32
+ },
+ )?;
+
+ // --- CAPACIDAD 3 :: sys_object_put(datos, datos_len, hijos, hijos_cnt, salida) -> i32 ---
+ // Graba un objeto en el grafo. El modulo entrega, en su memoria lineal, la
+ // carga util y un arreglo de `hijos_cnt` hashes de 32 bytes (las aristas).
+ // El kernel escribe el hash resultante —la identidad del objeto— en
+ // `salida`. Devuelve 0 si el objeto se grabo (o ya existia), -1 si el
+ // almacenamiento fallo.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_object_put",
+ |mut caller: Caller<'_, ContextoCapacidades>,
+ datos_ptr: u32,
+ datos_len: u32,
+ hijos_ptr: u32,
+ hijos_cnt: u32,
+ salida: u32|
+ -> Result<i32, Error> {
+ let memoria = obtener_memoria(&caller)?;
+
+ // --- Leer las entradas de la memoria lineal, con limites firmes. ---
+ let (datos, hijos) = {
+ let m = memoria.data(&caller);
+
+ let datos = rango(
+ m,
+ datos_ptr,
+ datos_len as usize,
+ "WASM :: sys_object_put desbordo la memoria lineal (datos)",
+ )?
+ .to_vec();
+
+ // El arreglo de hijos: `hijos_cnt` hashes contiguos de 32 bytes.
+ let bytes_hijos = (hijos_cnt as usize).checked_mul(32).ok_or_else(|| {
+ Error::new("WASM :: sys_object_put con un conteo de hijos imposible")
+ })?;
+ let crudo = rango(
+ m,
+ hijos_ptr,
+ bytes_hijos,
+ "WASM :: sys_object_put desbordo la memoria lineal (hijos)",
+ )?;
+ let mut hijos: alloc::vec::Vec<Hash> =
+ alloc::vec::Vec::with_capacity(hijos_cnt as usize);
+ for trozo in crudo.chunks_exact(32) {
+ let mut h = [0u8; 32];
+ h.copy_from_slice(trozo);
+ hijos.push(h);
+ }
+
+ // Verificar que el hash de salida cabe ANTES de tocar el disco.
+ rango(
+ m,
+ salida,
+ 32,
+ "WASM :: sys_object_put desbordo la memoria lineal (salida)",
+ )?;
+
+ (datos, hijos)
+ };
+
+ // --- Grabar. Un fallo del almacen NO es culpa de la app: -1. ---
+ match crate::almacen::almacenar(datos, hijos) {
+ Ok(hash) => {
+ let m = memoria.data_mut(&mut caller);
+ m[salida as usize..salida as usize + 32].copy_from_slice(&hash);
+ Ok(0)
+ }
+ Err(_) => Ok(-1),
+ }
+ },
+ )?;
+
+ // --- CAPACIDAD 4 :: sys_object_datos(hash, salida, capacidad) -> i32 ---
+ // Copia la carga util del objeto `hash` en `salida`. Devuelve el numero de
+ // bytes copiados, o -1 si el objeto no existe, -2 si `capacidad` no basta,
+ // -3 si el almacenamiento fallo.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_object_datos",
+ |mut caller: Caller<'_, ContextoCapacidades>,
+ hash_ptr: u32,
+ salida: u32,
+ capacidad: u32|
+ -> Result<i32, Error> {
+ let memoria = obtener_memoria(&caller)?;
+
+ let hash = {
+ let m = memoria.data(&caller);
+ leer_hash(
+ m,
+ hash_ptr,
+ "WASM :: sys_object_datos desbordo la memoria lineal (hash)",
+ )?
+ };
+
+ let objeto = match crate::almacen::recuperar(&hash) {
+ Ok(Some(objeto)) => objeto,
+ Ok(None) => return Ok(-1),
+ Err(_) => return Ok(-3),
+ };
+ if objeto.datos.len() > capacidad as usize {
+ return Ok(-2);
+ }
+
+ // Verificar que el destino cabe, y solo entonces copiar.
+ {
+ let m = memoria.data(&caller);
+ rango(
+ m,
+ salida,
+ objeto.datos.len(),
+ "WASM :: sys_object_datos desbordo la memoria lineal (salida)",
+ )?;
+ }
+ let n = objeto.datos.len();
+ let m = memoria.data_mut(&mut caller);
+ m[salida as usize..salida as usize + n].copy_from_slice(&objeto.datos);
+ Ok(n as i32)
+ },
+ )?;
+
+ // --- CAPACIDAD 5 :: sys_object_hijo(hash, indice, salida) -> i32 ---
+ // Recorre las aristas del DAG. Devuelve el NUMERO de hijos del objeto
+ // `hash`; si `indice` es valido, ademas escribe el hash de ese hijo en
+ // `salida`. Devuelve -1 si el objeto no existe, -3 si el almacen fallo.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_object_hijo",
+ |mut caller: Caller<'_, ContextoCapacidades>,
+ hash_ptr: u32,
+ indice: u32,
+ salida: u32|
+ -> Result<i32, Error> {
+ let memoria = obtener_memoria(&caller)?;
+
+ let hash = {
+ let m = memoria.data(&caller);
+ leer_hash(
+ m,
+ hash_ptr,
+ "WASM :: sys_object_hijo desbordo la memoria lineal (hash)",
+ )?
+ };
+
+ let objeto = match crate::almacen::recuperar(&hash) {
+ Ok(Some(objeto)) => objeto,
+ Ok(None) => return Ok(-1),
+ Err(_) => return Ok(-3),
+ };
+ let total = objeto.hijos.len();
+
+ // Si el indice apunta a un hijo real, entregar su hash.
+ if let Some(hijo) = objeto.hijos.get(indice as usize) {
+ {
+ let m = memoria.data(&caller);
+ rango(
+ m,
+ salida,
+ 32,
+ "WASM :: sys_object_hijo desbordo la memoria lineal (salida)",
+ )?;
+ }
+ let m = memoria.data_mut(&mut caller);
+ m[salida as usize..salida as usize + 32].copy_from_slice(hijo);
+ }
+ Ok(total as i32)
+ },
+ )?;
+
+ // --- CAPACIDAD 6 :: sys_object_raiz(salida) -> i32 ---
+ // Escribe en `salida` el hash de la raiz del grafo. Devuelve 1 si hay
+ // raiz, 0 si el grafo aun no tiene ninguna.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_object_raiz",
+ |mut caller: Caller<'_, ContextoCapacidades>, salida: u32| -> Result<i32, Error> {
+ let memoria = obtener_memoria(&caller)?;
+ match crate::almacen::raiz() {
+ Some(hash) => {
+ {
+ let m = memoria.data(&caller);
+ rango(
+ m,
+ salida,
+ 32,
+ "WASM :: sys_object_raiz desbordo la memoria lineal (salida)",
+ )?;
+ }
+ let m = memoria.data_mut(&mut caller);
+ m[salida as usize..salida as usize + 32].copy_from_slice(&hash);
+ Ok(1)
+ }
+ None => Ok(0),
+ }
+ },
+ )?;
+
+ // --- CAPACIDAD 7 :: sys_object_fijar_raiz(hash) -> i32 ---
+ // Corona el objeto `hash` como raiz del grafo. Devuelve 0 si se logro, -3
+ // si el almacenamiento fallo.
+ enlazador.func_wrap(
+ "renaser",
+ "sys_object_fijar_raiz",
+ |caller: Caller<'_, ContextoCapacidades>, hash_ptr: u32| -> Result<i32, Error> {
+ let memoria = obtener_memoria(&caller)?;
+ let hash = {
+ let m = memoria.data(&caller);
+ leer_hash(
+ m,
+ hash_ptr,
+ "WASM :: sys_object_fijar_raiz desbordo la memoria lineal (hash)",
+ )?
+ };
+ match crate::almacen::fijar_raiz(hash) {
+ Ok(()) => Ok(0),
+ Err(_) => Ok(-3),
+ }
+ },
+ )?;
+
+ Ok(())
+}
diff --git a/renaser/kernel/src/wasm/mod.rs b/renaser/kernel/src/wasm/mod.rs
new file mode 100644
index 0000000..5710331
--- /dev/null
+++ b/renaser/kernel/src/wasm/mod.rs
@@ -0,0 +1,200 @@
+// =============================================================================
+// renaser :: kernel/src/wasm — Fase 4/5 :: el escudo de aislamiento (WASM)
+// -----------------------------------------------------------------------------
+// Aqui renaser sustituye las costosas fronteras de hardware (la MMU, los
+// anillos de la CPU) por limites MATEMATICOS sobre el bytecode. Una aplicacion
+// WebAssembly se ejecuta en su propia memoria lineal; sus unicas puertas al
+// exterior son las capacidades que el enlazador del host le concede. Lo que
+// no este importado no existe: no hay camino fisico para ejecutarlo.
+//
+// FASE 5 :: el aislamiento deja de ser solo ESPACIAL (memoria) y pasa a ser
+// tambien TEMPORAL (tiempo de CPU). Cada `tick` se ejecuta con un presupuesto
+// estricto de COMBUSTIBLE (fuel): si una app lo agota —un bucle infinito, un
+// trabajo desmedido—, el runtime lanza una trampa, el kernel recupera el mando
+// y la desaloja. Ningun modulo, por discolo que sea, secuestra el procesador.
+// =============================================================================
+
+pub mod env;
+
+use wasmi::{
+ CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc,
+};
+
+use crate::grafico::{Color, RegionPantalla};
+use env::ContextoCapacidades;
+
+/// Combustible concedido a `init`. Cubre con holgura el pintado inicial del
+/// fondo de una region a pantalla casi completa — un gasto unico, de arranque.
+const FUEL_ARRANQUE: u64 = 20_000_000;
+
+/// Combustible concedido a cada `tick`. Sobra para un fotograma honesto (unos
+/// cientos de miles de operaciones); una app en bucle infinito lo agota en
+/// milisegundos y es desalojada. Este numero ES el techo temporal del userspace.
+const FUEL_FOTOGRAMA: u64 = 2_000_000;
+
+/// Techo de memoria lineal por aplicacion: 4 MiB. Un modulo que intente crecer
+/// su memoria mas alla es desalojado — el aislamiento ESPACIAL del userspace,
+/// gemelo del techo TEMPORAL que impone el combustible.
+const TECHO_MEMORIA: usize = 4 * 1024 * 1024;
+
+/// Por que el kernel da por terminada —desaloja— una aplicacion WASM.
+#[derive(Clone, Copy)]
+pub enum FallaApp {
+ /// El modulo no se pudo cargar, validar, enlazar o instanciar.
+ Carga,
+ /// La aplicacion agoto su combustible dentro de un `tick`: bucle infinito
+ /// o trabajo desmedido. El guardarrail TEMPORAL en accion.
+ SinCombustible,
+ /// La aplicacion intento crecer su memoria lineal mas alla de su cuota.
+ /// El guardarrail ESPACIAL en accion.
+ SinMemoria,
+ /// La aplicacion ejecuto una trampa: acceso fuera de limites, instruccion
+ /// `unreachable`, una capacidad violada... su propio codigo la abortó.
+ Trampa,
+}
+
+impl FallaApp {
+ /// El color de la baliza de desalojo segun la causa de la falla: amarillo
+ /// palido si reviento su techo de memoria, purpura para cualquier otra.
+ pub fn color_baliza(self) -> Color {
+ match self {
+ FallaApp::SinMemoria => Color::DESALOJO_MEMORIA,
+ _ => Color::DESALOJO,
+ }
+ }
+}
+
+/// Una aplicacion WebAssembly viva: su estado PERSISTE entre fotogramas. A
+/// diferencia de la Fase 4 —que instanciaba y cedia el control de un gesto—,
+/// aqui la instancia se conserva y el kernel la hace avanzar `tick` a `tick`.
+pub struct AplicacionWasm {
+ /// El almacen: todo el estado de ESTA instancia — su memoria lineal, sus
+ /// globales y el contexto de capacidades con su region de pantalla.
+ almacen: Store<ContextoCapacidades>,
+ /// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
+ /// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
+ /// el handle de la `Instance` no aporta nada y no se retiene.
+ func_tick: TypedFunc<(), ()>,
+ /// La region de pantalla de la app — su ventana, y donde se tatua su baliza
+ /// de desalojo si llega a fallar.
+ region: RegionPantalla,
+}
+
+impl AplicacionWasm {
+ /// Carga, valida, instancia y arranca una aplicacion WASM aislada, ligada a
+ /// una region de pantalla. Si algo falla en el camino, se devuelve la falla
+ /// en lugar de incendiar el kernel.
+ ///
+ /// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una
+ /// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
+ /// el reactor en cada pulso del reloj.
+ pub fn cargar(bytecode: &[u8], region: RegionPantalla) -> Result<AplicacionWasm, FallaApp> {
+ // 1. El motor, con metricas de combustible y compilacion ANTICIPADA: la
+ // traduccion del modulo ocurre ahora, de modo que el `fuel` mida
+ // despues solo EJECUCION, jamas compilacion diferida.
+ let mut config = Config::default();
+ config.consume_fuel(true);
+ config.compilation_mode(CompilationMode::Eager);
+ let motor = Engine::new(&config);
+
+ // 2. Validar y traducir el modulo — ya instrumentado con fuel.
+ let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?;
+
+ // 3. El almacen, con el contexto de capacidades de ESTA app: su region
+ // de pantalla, su canal de teclado y su techo de memoria. El canal
+ // se crea ahora pero se inscribe en la difusion de la IRQ1 al final,
+ // ya con la app cargada: una carga fallida no deja canales huerfanos.
+ let canal = crate::async_system::teclado::crear_canal();
+ let limites = StoreLimitsBuilder::new()
+ .memory_size(TECHO_MEMORIA)
+ // Una expansion denegada se convierte en TRAMPA, no en un -1 que la
+ // app pudiera ignorar: asi el kernel la captura y la desaloja.
+ .trap_on_grow_failure(true)
+ .build();
+ let mut almacen = Store::new(
+ &motor,
+ ContextoCapacidades {
+ region,
+ canal,
+ limites,
+ },
+ );
+ // Ligar el limitador de recursos: `wasmi` lo consultara en cada
+ // `memory.grow`, tambien durante la instanciacion.
+ almacen.limiter(|contexto| &mut contexto.limites);
+ // Dotar de combustible ANTES de instanciar: la instanciacion no debe
+ // quedarse a cero y abortar.
+ almacen.set_fuel(FUEL_ARRANQUE).map_err(|_| FallaApp::Carga)?;
+
+ // 4. El enlazador y la matriz de capacidades (ver `env`).
+ let mut enlazador: Linker<ContextoCapacidades> = Linker::new(&motor);
+ env::enlazar_capacidades(&mut enlazador).map_err(|_| FallaApp::Carga)?;
+
+ // 5. Instanciar, resolviendo las importaciones contra las capacidades.
+ let instancia = enlazador
+ .instantiate_and_start(&mut almacen, &modulo)
+ .map_err(|_| FallaApp::Carga)?;
+
+ // 6. Resolver los dos puntos del ABI de fotograma: `init` y `tick`.
+ let func_init = instancia
+ .get_typed_func::<(), ()>(&almacen, "init")
+ .map_err(|_| FallaApp::Carga)?;
+ let func_tick = instancia
+ .get_typed_func::<(), ()>(&almacen, "tick")
+ .map_err(|_| FallaApp::Carga)?;
+
+ // 7. Arranque unico: `init` prepara el estado inicial de la aplicacion.
+ almacen.set_fuel(FUEL_ARRANQUE).map_err(|_| FallaApp::Carga)?;
+ func_init
+ .call(&mut almacen, ())
+ .map_err(|_| FallaApp::Carga)?;
+
+ // 8. Con la app ya cargada e instanciada, inscribir su canal de teclado
+ // en la difusion de la IRQ1: desde aqui recibe cada pulsacion.
+ crate::async_system::teclado::registrar_canal(&almacen.data().canal);
+
+ Ok(AplicacionWasm {
+ almacen,
+ func_tick,
+ region,
+ })
+ }
+
+ /// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de
+ /// combustible y le cede el control con `tick`. Si la app lo agota o ejecuta
+ /// una trampa, el kernel recupera el mando y la falla se devuelve para que
+ /// la tarea proceda al desalojo. El kernel nunca pierde el control.
+ pub fn tick(&mut self) -> Result<(), FallaApp> {
+ // Recargar el deposito: cada fotograma parte con su techo intacto.
+ self.almacen
+ .set_fuel(FUEL_FOTOGRAMA)
+ .map_err(|_| FallaApp::Trampa)?;
+
+ match self.func_tick.call(&mut self.almacen, ()) {
+ Ok(()) => Ok(()),
+ // `as_trap_code` da un codigo publico y univoco para cada causa:
+ // `OutOfFuel` pliega toda variante de agotamiento de combustible;
+ // `GrowthOperationLimited` es la cuota de memoria denegada.
+ Err(error) => match error.as_trap_code() {
+ Some(TrapCode::OutOfFuel) => Err(FallaApp::SinCombustible),
+ Some(TrapCode::GrowthOperationLimited) => Err(FallaApp::SinMemoria),
+ _ => Err(FallaApp::Trampa),
+ },
+ }
+ }
+
+ /// La region de pantalla asignada a la aplicacion.
+ pub fn region(&self) -> RegionPantalla {
+ self.region
+ }
+}
+
+/// Reconciliacion del ciclo de vida. Cuando una `AplicacionWasm` muere —porque
+/// fue desalojada y su tarea concluyo—, su canal de teclado debe darse de baja
+/// de la difusion de la IRQ1. Sin esto, el manejador de interrupciones seguiria
+/// empujando scancodes a una cola muerta: una fuga lenta pero segura.
+impl Drop for AplicacionWasm {
+ fn drop(&mut self) {
+ crate::async_system::teclado::cerrar_canal(&self.almacen.data().canal);
+ }
+}
diff --git a/renaser/rust-toolchain.toml b/renaser/rust-toolchain.toml
new file mode 100644
index 0000000..84f180a
--- /dev/null
+++ b/renaser/rust-toolchain.toml
@@ -0,0 +1,14 @@
+# =============================================================================
+# renaser :: fijacion del toolchain
+# -----------------------------------------------------------------------------
+# Nightly por dos razones que PERSISTEN: las dependencias de artefacto
+# (`-Zbindeps`) y el script de construccion de la crate `bootloader`, que
+# compila su propio cargador UEFI/BIOS y necesita `rust-src` + `llvm-tools`.
+# El kernel en si ya NO necesita `build-std`: usa el target nativo precompilado.
+# =============================================================================
+
+[toolchain]
+channel = "nightly"
+components = ["rust-src", "llvm-tools", "rustfmt", "clippy"]
+targets = ["x86_64-unknown-none", "x86_64-unknown-uefi"]
+profile = "minimal"