feat(renaser): Fase 11 — el reloj del sistema como capacidad de host

El userspace gana un sentido del tiempo: hasta ahora una app solo sabía
cuántas veces la habían llamado, no cuánto tiempo había pasado.

- Capacidad `sys_tiempo_mono() -> u64` — la décima función del host:
  los milisegundos monótonos desde el arranque. `reloj` expone la
  cuenta del PIT (100 Hz) como `milisegundos()`; `env` la inyecta.
  Lectura pura, no toca la memoria del módulo, jamás retrocede.
- App nueva `pulso` (`apps/pulso/`, wasm32): un compás visual cuya
  escena es una función PURA de `sys_tiempo_mono` — sin estado entre
  fotogramas—. Dos instancias laten al unísono nazcan cuando nazcan.
- `GENESIS` crece de 5 a 6 apps; `pulso` es la maestra del escritorio.

Verificado en QEMU (sendkey): la barra de `pulso` avanza con el tiempo
de pared; un segundo `pulso` lanzado con Alt+N ~15 s después aparece
sincronizado con el primero — el compás se rige por el reloj absoluto,
no por una cuenta de fotogramas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 20:43:17 +00:00
parent 19d04a2766
commit 4bcdc88c83
11 changed files with 269 additions and 11 deletions
+26
View File
@@ -847,3 +847,29 @@ una app puede nacer o cerrarse con el reactor ya en marcha.
re-tesela de 5 a 8 ventanas. Tres `Alt+Q` cierran la app enfocada una a una
y el teselado reclama su espacio, de 8 de vuelta a 5. El kernel sigue estable
a través de todas las altas y bajas.
## Fase 11 — El reloj del sistema como capacidad de host — 2026-05-22
Hasta la Fase 10 una aplicación sólo sabía CUÁNTAS veces la habían llamado —un
`tick` tras otro—, no CUÁNTO tiempo había pasado. La Fase 11 le da al userspace
un sentido del tiempo: el reloj monótono del sistema, como capacidad.
### Añadido
- **Capacidad `sys_tiempo_mono() -> u64`** — la décima función del host. Los
milisegundos transcurridos desde el arranque. El temporizador (PIT) ya late a
100 Hz; `reloj` expone esa cuenta como `milisegundos()` y `env` la inyecta.
Es una lectura PURA —no toca la memoria lineal del módulo, no hay puntero que
validar— y MONÓTONA: jamás retrocede.
- **App `pulso`** (`apps/pulso/`, `wasm32`). Un compás visual: una cabeza
brillante que recorre una pista y vuelve, con un período de 6 s. Su escena es
una FUNCIÓN PURA de `sys_tiempo_mono` —no guarda estado entre fotogramas—.
De ahí su prueba: dos instancias de `pulso`, nazca una al arrancar y otra
mucho después con un `Alt+N`, laten exactamente al unísono.
- `pulso` se suma al userspace de génesis: `GENESIS` pasa de 5 a 6 apps, con
`pulso` como la primera —la ventana maestra del escritorio—.
### Verificado
- QEMU (`sendkey`): la barra de `pulso` avanza con el tiempo de pared entre dos
capturas. Un `Alt+N` da a luz un segundo `pulso` ~15 s después del arranque;
flotado junto al primero, su barra está en la MISMA fase — la prueba de que
el compás se rige por el reloj absoluto, no por una cuenta de fotogramas.
+4 -3
View File
@@ -28,7 +28,7 @@ Reconstruir una app WASM del userspace tras tocarla. Los `.wasm` viven en
modulo `hello_wasm` se copia como `app.wasm`, el resto conserva su nombre:
```sh
cd apps/<app> # hello_wasm, discola, glotona, cronista, memoriosa
cd apps/<app> # hello_wasm, discola, glotona, cronista, memoriosa, pulso
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/<app>.wasm ../../kernel/assets/<app>.wasm
# (hello_wasm es la excepcion: su destino es kernel/assets/app.wasm)
@@ -78,8 +78,9 @@ objetos—, la Fase 8 COMPLETA —el compositor teselante e interactivo: teselad
con `mirada-layout` (8a), ciclado de layout (8b), foco y enrutamiento selectivo
del teclado (8c), promoción y reordenación de ventanas (8d)—, la Fase 9
COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—
y la Fase 10 COMPLETA —alta y baja de aplicaciones en vivo (`Alt+N` / `Alt+Q`)—.
Todo verificado en QEMU. Ver `ROADMAP.md`.
la Fase 10 COMPLETA —alta y baja de aplicaciones en vivo (`Alt+N` / `Alt+Q`)— y
la Fase 11 COMPLETA —el reloj del sistema como capacidad de host
(`sys_tiempo_mono`) + la app `pulso`—. Todo verificado en QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo
+20
View File
@@ -434,6 +434,26 @@ y no volvía a tocar la lista. Ahora tiene una bandeja donde van dejándose los
recién llegados, y en cada ronda la mira y les da su sitio. La casa ya no se
escribe entera de una vez: se va escribiendo, día a día, mientras se vive.
## El sentido del tiempo — un reloj para los inquilinos
Los inquilinos de la casa sabían hacer su trabajo, turnarse, recordar entre
sesiones. Pero había algo que no sabían: qué hora era. Vivían en un presente
sin medida —cada uno contaba las veces que lo habían despertado, y nada más—.
Dos inquilinos que empezaran la misma tarea en momentos distintos no tenían
forma de ir a la par.
Hoy la casa les regaló un reloj. No uno para cada cuarto: uno solo, en el
recibidor, que todos pueden mirar. Marca, sin más, cuánto hace que la casa
despertó. Quien quiera saber la hora, la mira; quien no, sigue a lo suyo.
Y para estrenarlo llegó un inquilino nuevo, «pulso», que no hace otra cosa que
mirar ese reloj y dibujar su paso: una luz que recorre un riel, una y otra vez,
con un compás de seis segundos. Lo hermoso es lo que ocurre al invitar a un
segundo «pulso» mucho después: no empieza su recorrido desde el principio —se
incorpora justo donde va el primero, al instante, como dos bailarines que oyen
la misma música—. Porque ninguno lleva su propia cuenta: ambos miran el mismo
reloj del recibidor.
---
*El diario continúa. La próxima página la escribirá la próxima jornada.*
+16 -2
View File
@@ -181,8 +181,22 @@ Verificada en QEMU (`sendkey`).
- El censo de ventanas sólo crece —los índices son la identidad, jamás se
reciclan—; una ventana cerrada queda como ranura inerte, fuera del teselado.
Líneas abiertas posteriores: más capacidades del host (temporización, audio);
reciclado de las ranuras de ventana cerradas.
## Fase 11 — el reloj del sistema como capacidad de host (completada)
Hasta la Fase 10 una aplicación sólo sabía cuántas veces la habían llamado, no
cuánto tiempo había pasado. La Fase 11 le da al userspace un sentido del
tiempo. Verificada en QEMU (`sendkey`).
- Capacidad `sys_tiempo_mono`: los milisegundos monótonos desde el arranque —la
décima función del host—. El temporizador (PIT) ya late a 100 Hz; `reloj`
expone esa cuenta y `env` la inyecta. Lectura pura, jamás retrocede.
- App nueva `pulso`: un compás visual cuya escena es una función PURA del reloj
del host. Dos instancias, nazcan cuando nazcan, laten al unísono — la prueba
de que el tiempo es absoluto, no una cuenta de fotogramas.
- El userspace de génesis crece de 5 a 6 apps.
Líneas abiertas posteriores: audio como capacidad de host; reciclado de las
ranuras de ventana cerradas.
## Principios que persisten entre fases
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "pulso"
version = "0.1.0"
+29
View File
@@ -0,0 +1,29 @@
# =============================================================================
# renaser :: apps/pulso — Fase 11 :: el compas visual del reloj del host
# -----------------------------------------------------------------------------
# Un modulo WebAssembly puro que no guarda estado alguno: su escena es una
# funcion pura del reloj monotono del sistema (`sys_tiempo_mono`). Por eso dos
# instancias de `pulso`, nazcan cuando nazcan, laten al unisono. Tiene su
# propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
# =============================================================================
[package]
name = "pulso"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM — un compas visual gobernado por el reloj del host"
[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
+137
View File
@@ -0,0 +1,137 @@
// =============================================================================
// renaser :: apps/pulso — Fase 11 :: el compas visual del reloj del host
// -----------------------------------------------------------------------------
// Hasta la Fase 10 una aplicacion solo sabia CUANTAS veces la habian llamado
// —un `tick` tras otro—, no CUANTO tiempo habia pasado. La Fase 11 le da al
// userspace un reloj: la capacidad `sys_tiempo_mono`, los milisegundos
// monotonos desde el arranque.
//
// `pulso` es su testigo. Dibuja un compas —una cabeza brillante que recorre
// una pista y vuelve— cuya posicion es una FUNCION PURA del reloj del host.
// No guarda estado entre fotogramas: no le hace falta. Y de ahi su prueba:
// dos instancias de `pulso`, nazca una al arrancar y otra mucho despues con
// un `Alt+N`, laten EXACTAMENTE al unisono — porque ambas leen el mismo
// reloj, no un contador propio de fotogramas.
// =============================================================================
#![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);
/// El reloj monotono del sistema: milisegundos desde el arranque.
fn sys_tiempo_mono() -> u64;
}
/// 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 = 120;
/// El periodo del compas, en milisegundos: la cabeza recorre la pista entera y
/// vuelve a empezar cada `PERIODO`.
const PERIODO: u64 = 6000;
/// Azul nocturno: el fondo del lienzo.
const FONDO: u32 = 0x0A_18_30;
/// La pista por donde corre el compas — un surco tenue.
const PISTA: u32 = 0x1E_2A_44;
/// La estela ya recorrida en este ciclo.
const ESTELA: u32 = 0x2E_50_C8;
/// La cabeza del compas — el indigo brillante del foco del compositor.
const CABEZA: u32 = 0x8B_5C_F6;
/// Margen lateral de la pista, en pixeles.
const MARGEN: usize = 24;
/// Filas que ocupa la pista — un surco horizontal centrado en el lienzo.
const PISTA_Y0: usize = 46;
const PISTA_Y1: usize = 74;
/// Anchura de la cabeza del compas, en pixeles.
const CABEZA_ANCHO: usize = 7;
/// 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];
/// Preparacion: el kernel la invoca UNA sola vez, al cargar el modulo. Pinta el
/// primer fotograma — el compas en el instante de nacer.
#[no_mangle]
pub extern "C" fn init() {
pintar();
}
/// Un fotograma de trabajo: vuelve a pintar el compas en el instante actual y
/// RETORNA, cediendo la CPU al kernel y a las apps vecinas.
#[no_mangle]
pub extern "C" fn tick() {
pintar();
}
/// Pinta el compas en el instante ACTUAL. No guarda estado alguno: la escena es
/// una funcion pura del reloj del host. Por eso dos instancias de `pulso`,
/// nazcan cuando nazcan, laten EXACTAMENTE al unisono.
fn pintar() {
// 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) };
// La fase del compas: 0 al inicio del periodo, casi 1 al final. Se deriva
// SOLO del reloj del host — ni un contador propio, ni un origen guardado.
// SEGURIDAD: `sys_tiempo_mono` es una capacidad del host; no toca memoria.
let tiempo = unsafe { sys_tiempo_mono() };
let fase = tiempo % PERIODO;
// El ancho util de la pista y hasta donde ha llegado la cabeza.
let util = ANCHO - 2 * MARGEN;
let avance = (fase as usize * util) / PERIODO as usize;
// Fondo limpio.
for pixel in lienzo.iter_mut() {
*pixel = FONDO;
}
// La pista entera, tenue.
banda(lienzo, MARGEN, MARGEN + util, PISTA);
// La estela ya recorrida en este ciclo.
banda(lienzo, MARGEN, MARGEN + avance, ESTELA);
// La cabeza del compas: una barra brillante al frente de la estela.
let cabeza0 = MARGEN + avance;
banda(lienzo, cabeza0, cabeza0 + CABEZA_ANCHO, CABEZA);
volcar(lienzo);
}
/// Rellena la pista —las filas [`PISTA_Y0`, `PISTA_Y1`)— entre las columnas
/// [x0, x1) con un color, recortado con firmeza al ancho del lienzo.
fn banda(lienzo: &mut [u32], x0: usize, x1: usize, color: u32) {
let x0 = x0.min(ANCHO);
let x1 = x1.min(ANCHO);
let mut fila = PISTA_Y0;
while fila < PISTA_Y1 {
let base = fila * ANCHO;
let mut col = x0;
while col < x1 {
lienzo[base + col] = color;
col += 1;
}
fila += 1;
}
}
/// 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);
}
}
+7 -5
View File
@@ -107,11 +107,13 @@ struct AppGenesis {
region: (u32, u32, u32, u32),
}
/// El userspace de genesis — las cinco aplicaciones que pueblan un disco recien
/// forjado. Un saludo (`hola`), la `memoriosa` interactiva que recuerda entre
/// sesiones (Fase 7c), y tres demos de los guardarrailes del kernel: `discola`
/// (combustible), `glotona` (memoria) y `cronista` (la cronica de los arranques).
const GENESIS: [AppGenesis; 5] = [
/// El userspace de genesis — las seis aplicaciones que pueblan un disco recien
/// forjado. El compas visual `pulso` (Fase 11), un saludo (`hola`), la
/// `memoriosa` interactiva que recuerda entre sesiones (Fase 7c), y tres demos
/// de los guardarrailes del kernel: `discola` (combustible), `glotona`
/// (memoria) y `cronista` (la cronica de los arranques).
const GENESIS: [AppGenesis; 6] = [
AppGenesis { nombre: "pulso", archivo: "pulso.wasm", region: (100, 120, 360, 120) },
AppGenesis { nombre: "hola", archivo: "app.wasm", region: (100, 120, 480, 560) },
AppGenesis { nombre: "memoriosa", archivo: "memoriosa.wasm", region: (700, 120, 360, 80) },
AppGenesis { nombre: "discola", archivo: "discola.wasm", region: (60, 700, 360, 80) },
BIN
View File
Binary file not shown.
+7
View File
@@ -54,6 +54,13 @@ fn pulsos() -> u64 {
CONTADOR_PULSOS.load(Ordering::Acquire)
}
/// Milisegundos transcurridos desde el arranque (Fase 11). El temporizador late
/// a 100 Hz —un pulso cada 10 ms—; esta es la lectura del reloj MONOTONO que el
/// host ofrece al userspace como `sys_tiempo_mono`. Jamas retrocede.
pub fn milisegundos() -> u64 {
pulsos().saturating_mul(10)
}
/// Inscribe un waker en el censo de espera del proximo fotograma.
fn inscribir(waker: Waker) {
if let Some(censo) = EN_ESPERA.get() {
+16 -1
View File
@@ -13,7 +13,8 @@
// * sys_object_raiz — leer la raiz del grafo;
// * sys_object_fijar_raiz — coronar un objeto como raiz;
// * sys_estado_cargar — leer el estado persistido de la app (Fase 7c);
// * sys_estado_guardar — anclar el estado persistido de la app (Fase 7c).
// * sys_estado_guardar — anclar el estado persistido de la app (Fase 7c);
// * sys_tiempo_mono — leer el reloj monotono del sistema (Fase 11).
//
// 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
@@ -442,5 +443,19 @@ pub(crate) fn enlazar_capacidades(
},
)?;
// --- CAPACIDAD 10 :: sys_tiempo_mono() -> u64 ---
// El reloj MONOTONO del sistema: milisegundos transcurridos desde el
// arranque. Le da al userspace un sentido del tiempo independiente del
// ritmo de los fotogramas — una app sabe CUANTO ha pasado, no solo CUANTAS
// veces la han llamado—. Jamas retrocede. No toca la memoria del modulo:
// es una lectura pura, sin puntero que validar.
enlazador.func_wrap(
"renaser",
"sys_tiempo_mono",
|_caller: Caller<'_, ContextoCapacidades>| -> u64 {
crate::async_system::reloj::milisegundos()
},
)?;
Ok(())
}