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
+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);
}
}