From 5c462e6d307addd58614a8c4495f4becda04e4b2 Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 22 May 2026 19:19:21 +0000 Subject: [PATCH] =?UTF-8?q?feat(renaser):=20Fases=208b=20y=208c=20?= =?UTF-8?q?=E2=80=94=20el=20escritorio=20interactivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El compositor de la 8a teselaba, pero era inmovil. Las 8b/8c lo hacen vivo: el teclado reordena el escritorio y mueve el foco en caliente. - Cache de fotogramas: cada ventana guarda en RAM del kernel su ultimo fotograma —reservada una vez, acotada al lienzo natural—. Al re-teselar o mover el foco, el kernel recompone desde la cache: las apps que solo pintan en init (cronista) conservan su imagen sin enterarse del cambio. - compositor: el registro ESCRITORIO (ventanas, marcos, caches, modo); presentar_fotograma, desalojar, atender_mandos, ciclar_layout, mover_foco. Foco en un AtomicUsize, mandos en una cola lock-free. - teclado: la IRQ1 deja de difundir. Alt es el modificador del sistema — Alt+Espacio cicla el teselado, Alt+J/K mueven el foco—; una tecla ordinaria va SOLO a la app enfocada (CANALES reindexado por indice_app). - consola: borde de foco (indigo / gris) en cada marco. Guardarrail anti-interbloqueo: la IRQ1 jamas bloquea ESCRITORIO; se comunica por dos atomicos y una cola lock-free. Las caches se reservan una sola vez, al tamaño natural — sin asignacion en el bucle del reactor. Verificado en QEMU (screendump + sendkey): arranque teselado con hola enfocada; Alt+Espacio cicla a CenteredMaster y las apps estaticas conservan su contenido; Alt+J mueve el foco; las teclas llegan solo a la app enfocada. Cierra la Fase 8 — el compositor teselante e interactivo. Co-Authored-By: Claude Opus 4.7 --- renaser/CHANGELOG.md | 59 ++++ renaser/CLAUDE.md | 10 +- renaser/DIARIO.md | 30 ++ renaser/FASE8.md | 32 +- renaser/ROADMAP.md | 16 +- renaser/kernel/src/async_system/teclado.rs | 126 ++++++-- renaser/kernel/src/compositor.rs | 334 +++++++++++++++++++-- renaser/kernel/src/consola.rs | 67 +++-- renaser/kernel/src/grafico.rs | 16 + renaser/kernel/src/main.rs | 82 ++--- renaser/kernel/src/wasm/env.rs | 22 +- renaser/kernel/src/wasm/mod.rs | 59 ++-- 12 files changed, 676 insertions(+), 177 deletions(-) diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md index 245bbc2..e547907 100644 --- a/renaser/CHANGELOG.md +++ b/renaser/CHANGELOG.md @@ -672,3 +672,62 @@ COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA con centrado en su panel; las apps desalojadas (discola, glotona) tiñen su marco entero con la baliza. El registro de arranque permanece legible en la franja superior de la consola. + +## Fases 8b y 8c — El escritorio interactivo — 2026-05-22 + +El compositor de la Fase 8a teselaba, pero era inmóvil. Las Fases 8b y 8c lo +hacen VIVO: el teclado reordena el escritorio en caliente y mueve el foco. Y +para que las apps estáticas no se queden atrás en una reordenación, el kernel +asume la persistencia visual con una caché de fotogramas. + +### Añadido +- **Caché de fotogramas (backbuffer).** Cada ventana del compositor guarda, en + RAM del kernel, el último fotograma que su app envió. La caché se reserva UNA + vez, acotada al lienzo natural de la app —jamás crece—. Al re-teselar o mover + el foco, el kernel recompone cada ventana desde su caché: una app que sólo + pintó en su `init` —la cronista— conserva su imagen intacta a través de + cualquier reordenación, sin enterarse del cambio geométrico. +- **`compositor`: el escritorio interactivo.** Un registro `ESCRITORIO` —las + ventanas, sus marcos, sus cachés, el modo de teselado—; `presentar_fotograma` + (cachea y compone), `desalojar`, `atender_mandos`, `ciclar_layout`, + `mover_foco`. El foco vive en un `AtomicUsize`; los mandos del teclado, en + una cola lock-free. +- **`tarea_compositor`** — atiende los mandos del teclado en cada fotograma del + reactor: el contexto cooperativo donde es seguro re-teselar y recomponer. +- `Color::FOCO` (índigo brillante) y `Color::SIN_FOCO` (gris mate) — el borde + de cada ventana, que delata de un vistazo quién tiene el foco. + +### Cambiado +- **Teclado (Fase 8c).** El manejador de IRQ1 deja de difundir a ciegas: + - La tecla **Alt** es el modificador del sistema. `Alt+Espacio` cicla el + modo de teselado; `Alt+J` / `Alt+K` mueven el foco. Estos mandos se + consumen en la IRQ — jamás llegan a una app. + - Una tecla ordinaria se entrega SÓLO a la app ENFOCADA. El censo de canales + se reindexó por `indice_app` (`Vec>`): el foco —un + índice— elige el canal exacto. +- `consola::volcar_marco` y `pintar_desalojo` trazan el borde de foco de la + ventana. `sys_render_frame` ya no compone directo: delega en + `compositor::presentar_fotograma`, que cachea antes de componer. +- `AplicacionWasm` / `ContextoCapacidades` pierden el `marco` —ahora vive en el + registro del compositor, mutable—; conservan sólo el `indice_app`. + +### Exclusión de interrupciones (guardarraíl) +- El registro `ESCRITORIO` lo tocan SÓLO tareas cooperativas; el manejador de + IRQ1 jamás lo bloquea. La IRQ se comunica con el mundo cooperativo por un + canal estrecho y a prueba de interbloqueos: dos atómicos —el foco y el estado + de Alt— y una cola lock-free de mandos. Ningún cerrojo en disputa entre la + interrupción y una tarea. +- Las cachés se reservan en `compositor::fundar`, una sola vez, al tamaño + natural de cada app: ninguna asignación dinámica dentro del bucle del reactor. + +### Verificado +- QEMU (captura headless + `sendkey` por el monitor): + - **Arranque** — cinco apps teseladas en `MasterStack`; `hola` enfocada luce + el borde índigo, las demás el gris. + - **`Alt+Espacio`** — el escritorio cicla a `CenteredMaster`; discola, + glotona y cronista conservan su contenido a través del re-teselado: la + caché de fotogramas en acción. + - **`Alt+J`** — el foco salta de `hola` a `memoriosa`; el borde índigo se + mueve con él. + - **Enrutamiento** — con `memoriosa` enfocada, cuatro pulsaciones llegan sólo + a ella —cuatro celdas violetas—; las demás apps, intactas. diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md index 1f80ce4..9ef95d8 100644 --- a/renaser/CLAUDE.md +++ b/renaser/CLAUDE.md @@ -73,11 +73,11 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG). ## Estado -Fases 1 a 5, 6.0, 6.1, 6.2 y la Fase 7 COMPLETA —el userspace nace del grafo -de objetos: Manifiesto de Génesis (7a), imagen sembrada por `boot` (7b) y -persistencia inter-sesión por-app (7c)—. Fase 8 en curso: 8a hecha —el -compositor tesela las ventanas con `mirada-layout`—. Todo verificado en QEMU. -Ver `ROADMAP.md`. +Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de +objetos— y la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado +con `mirada-layout` (8a), `Alt+Espacio` cicla el layout (8b), foco con +`Alt+J`/`Alt+K` y enrutamiento selectivo del teclado (8c)—. Todo verificado en +QEMU. Ver `ROADMAP.md`. ## Flujo de trabajo diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md index 8168f83..7f763a6 100644 --- a/renaser/DIARIO.md +++ b/renaser/DIARIO.md @@ -342,6 +342,36 @@ de siempre; es el arquitecto quien decide en qué pared colgarlo, y la casa quien lo centra con cuidado en el espacio que le tocó. Nadie tuvo que cambiar para vivir mejor repartido. +## La casa atiende + +El arquitecto sabía repartir los cuartos, pero su plano era de piedra: una vez +puesto, no se movía. Hoy la casa aprendió a obedecer. Quien la habita puede +pedirle, con un gesto del teclado, que reordene las habitaciones —de una +disposición con una pieza grande a un lado a otra con la pieza central—; y el +arquitecto, en un parpadeo, las reparte de nuevo. + +Pero reordenar tiene un peligro. Algunos inquilinos pintan su cuadro una sola +vez, al llegar, y luego se quedan quietos —la cronista es así—. Si la casa +moviera su cuarto, la pared nueva quedaría en blanco: el inquilino, dormido, no +sabría que debe volver a pintar. La casa lo resolvió con delicadeza: de cada +cuarto guarda una fotografía del último cuadro. Cuando reordena, no despierta a +nadie; descuelga las fotografías y las vuelve a colgar, ya encuadradas, en las +paredes nuevas. El que dormía sigue durmiendo, y su obra reaparece intacta. + +Y la casa aprendió, además, a mirar. Ahora hay siempre un cuarto ENFOCADO —el +que tiene la atención—, y se le reconoce por un marco de luz índigo; los demás +llevan un marco gris, discreto. Con dos teclas se pasea esa luz de un inquilino +a otro. No es un adorno: es una decisión. Porque desde hoy, cuando alguien +teclea, sus palabras ya no se gritan a toda la casa —como hasta ayer—; se +entregan, en privado, sólo al inquilino que tiene el foco. Se le habla a quien +se mira. + +Detrás de esa cortesía hay una disciplina estricta. La campanilla del teclado +—la interrupción— es impaciente y no puede esperar a nadie; por eso jamás toca +los libros de la casa. Sólo deja una nota breve en un casillero a prueba de +prisas, y sigue su camino. Es el arquitecto quien, más tarde y con calma, lee +la nota y mueve los tabiques. Nadie se pisa; nada se traba. + --- *El diario continúa. La próxima página la escribirá la próxima jornada.* diff --git a/renaser/FASE8.md b/renaser/FASE8.md index 0e4ed24..f58829e 100644 --- a/renaser/FASE8.md +++ b/renaser/FASE8.md @@ -29,21 +29,29 @@ bare-metal. - `EntradaApp.region_ancho/alto` pasa a significar el tamaño NATURAL del lienzo de la app; `region_x/y` quedan vestigiales — el compositor decide la posición. -### 8b — Teselado interactivo +### 8b — Teselado interactivo — ✅ HECHA -- El teclado cicla en caliente los siete modos de `mirada-layout` - (`MasterStack`, `CenteredMaster`, `Spiral`, `Grid`, `Columns`, `Rows`, - `Monocle`). -- Re-teselar exige re-componer: las apps que sólo pintan en `init` necesitan - una señal de redibujado, o el kernel debe conservar su último fotograma para - recomponerlo en el marco nuevo. +- `Alt+Espacio` cicla en caliente los siete modos de teselado de + `mirada-layout` (`MasterStack`, `CenteredMaster`, `Spiral`, …). +- Re-teselar exige re-componer. La solución: el kernel guarda una CACHÉ del + último fotograma de cada app —acotada al lienzo natural, reservada una sola + vez— y recompone desde ella. Una app que sólo pintó en su `init` conserva su + imagen a través de cualquier reordenación, sin enterarse del cambio. -### 8c — Foco y `Workspace` +### 8c — Foco y enrutamiento selectivo — ✅ HECHA -- Adoptar el `Workspace` de `mirada-layout`: foco, orden-Z, alta y baja de - ventanas en vivo. -- Una ventana enfocada, resaltada; el teclado mueve el foco y promueve apps al - área maestra. +- Una ventana ENFOCADA, con borde índigo brillante; las demás, borde gris + mate. `Alt+J` / `Alt+K` pasean el foco entre las ventanas vivas. +- El teclado deja de difundir a ciegas: una tecla ordinaria se entrega sólo a + la app enfocada; los mandos `Alt+…` se consumen en la IRQ. +- El registro `ESCRITORIO` del compositor cumple el papel del `Workspace` de + `mirada-layout` —ventanas, marcos, foco— sin adoptar su tipo: el kernel + necesita además la caché de respaldo, que el `Workspace` no contempla. + +### Pendiente + +- Orden-Z y solapamiento (ventanas flotantes); alta y baja de apps en vivo. +- Promover la app enfocada al área maestra (`Alt+Enter`). ## Estructura de archivos diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md index bb95b60..5b82a51 100644 --- a/renaser/ROADMAP.md +++ b/renaser/ROADMAP.md @@ -120,7 +120,7 @@ destierra — las aplicaciones pasan a ser objetos del grafo, gobernadas por un y la app retoma donde quedó. Capacidades `sys_estado_cargar` / `sys_estado_guardar`; el kernel custodia un manifiesto VIVO y mutable. -## Fase 8 — el compositor teselante +## Fase 8 — el compositor teselante e interactivo (completada) El kernel deja de colocar las ventanas a mano: las **tesela**. El motor es `mirada-layout` —el mismo núcleo `no_std` que ordena el compositor Wayland de @@ -132,12 +132,16 @@ en `FASE8.md`. de cada app dentro de su marco; las apps no cambian una instrucción. `region_x/y` del manifiesto quedan vestigiales — la posición la decide el compositor. -- **8b — teselado interactivo (pendiente).** El teclado cicla en caliente los - siete modos de teselado de `mirada-layout`. -- **8c — foco y `Workspace` (pendiente).** Foco, orden-Z y alta/baja de - ventanas en vivo, con el `Workspace` de `mirada-layout`. +- **8b — teselado interactivo (completada).** `Alt+Espacio` cicla los modos de + teselado en caliente. El kernel cachea el último fotograma de cada app y + recompone desde la caché: las apps estáticas sobreviven al re-teselado sin + enterarse del cambio. +- **8c — foco y enrutamiento selectivo (completada).** Una ventana enfocada, + con borde índigo; `Alt+J` / `Alt+K` mueven el foco entre las ventanas vivas. + El teclado deja de difundir: entrega cada tecla sólo a la app enfocada. -Líneas abiertas posteriores: más capacidades del host (temporización, audio). +Líneas abiertas posteriores: orden-Z y ventanas flotantes; más capacidades del +host (temporización, audio). ## Principios que persisten entre fases diff --git a/renaser/kernel/src/async_system/teclado.rs b/renaser/kernel/src/async_system/teclado.rs index 707d441..b531950 100644 --- a/renaser/kernel/src/async_system/teclado.rs +++ b/renaser/kernel/src/async_system/teclado.rs @@ -1,16 +1,28 @@ // ============================================================================= // renaser :: async_system/teclado.rs — el canal de scancodes del teclado // ----------------------------------------------------------------------------- -// El manejador de IRQ1 es un mero PRODUCTOR: deposita cada scancode en colas +// El manejador de IRQ1 es el 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. +// FASE 5 :: cada app abre su PROPIO canal; la primera en sondear no le roba la +// pulsacion a las demas. +// +// FASE 8c :: el teclado deja de DIFUNDIR a ciegas. Ahora discrimina: +// +// * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make +// codes son MANDOS del compositor (ciclar el teselado, mover el foco): se +// consumen aqui, jamas llegan a una app. +// * Una tecla ordinaria se entrega SOLO a la app ENFOCADA — la que el +// compositor senala. El censo de canales se indexa por el `indice_app`, +// de modo que el foco —un atomico— elija el canal exacto. +// +// Todo esto corre en contexto de IRQ y NO bloquea ningun cerrojo cooperativo: +// el modificador es un atomico, los mandos van a una cola lock-free. // ============================================================================= +use core::sync::atomic::{AtomicBool, Ordering}; + use alloc::sync::Arc; use alloc::vec::Vec; @@ -18,16 +30,33 @@ use crossbeam_queue::ArrayQueue; use spin::{Mutex, Once}; use x86_64::instructions::interrupts; +use crate::compositor::{self, Mando}; + /// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto. const CAPACIDAD_COLA: usize = 256; +// --- Scancodes del set 1 que el teclado interpreta como mandos del sistema. --- +/// Alt izquierda — make (pulsada) y break (soltada). +const ALT_MAKE: u8 = 0x38; +const ALT_BREAK: u8 = 0xB8; +/// Barra espaciadora — `Alt + Espacio` cicla el modo de teselado. +const ESPACIO: u8 = 0x39; +/// Tecla J — `Alt + J` mueve el foco a la ventana siguiente. +const TECLA_J: u8 = 0x24; +/// Tecla K — `Alt + K` mueve el foco a la ventana anterior. +const TECLA_K: u8 = 0x25; + /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion. pub type CanalTeclado = Arc>; -/// 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>> = Once::new(); +/// Censo de canales, INDEXADO por el `indice_app` de cada aplicacion. Una +/// ranura `None` es una app que no abrio canal o que fue desalojada. El +/// indexado estable permite que el foco —un simple indice— elija el canal. +static CANALES: Once>>> = Once::new(); + +/// ¿Esta la tecla Alt pulsada? El modificador de los mandos del sistema. Lo +/// escribe y lo lee SOLO el manejador de IRQ1 — un atomico, sin cerrojo. +static ALT_PULSADO: AtomicBool = AtomicBool::new(false); /// Funda el censo de canales del teclado. Requiere el heap ya activo; debe /// invocarse una sola vez, antes de habilitar las interrupciones. @@ -35,40 +64,79 @@ pub fn init() { CANALES.call_once(|| Mutex::new(Vec::new())); } -/// Crea un canal de teclado nuevo, AUN sin inscribir en la difusion. Cada +/// Crea un canal de teclado nuevo, AUN sin inscribir en el censo. 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) { +/// Inscribe el canal de la app `indice` en el censo. Desde este instante, una +/// tecla ordinaria llega a esta app cuando tiene el foco. Se invoca al final de +/// la carga de una app: una carga fallida no debe dejar canales huerfanos. +pub fn registrar_canal(indice: usize, 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)); + let mut censo = censo.lock(); + while censo.len() <= indice { + censo.push(None); + } + censo[indice] = Some(canal.clone()); }); } } -/// 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) { +/// Da de baja el canal de la app `indice`. Lo invoca el `Drop` de una +/// aplicacion desalojada: la ranura queda en `None` y la IRQ deja de enrutarle +/// teclas, sin desplazar los indices de las demas. +pub fn cerrar_canal(indice: usize) { if let Some(censo) = CANALES.get() { - for canal in censo.lock().iter() { - // Si un canal desborda, se descarta el scancode en silencio: mas + interrupts::without_interrupts(|| { + let mut censo = censo.lock(); + if let Some(ranura) = censo.get_mut(indice) { + *ranura = None; + } + }); + } +} + +/// Punto de entrada DESDE el manejador de IRQ1. Rastrea el modificador Alt, +/// intercepta los mandos del sistema y enruta la tecla ordinaria a la app +/// enfocada. Deliberadamente breve y libre de panicos: corre en contexto de +/// interrupcion y no bloquea ningun cerrojo cooperativo. +pub fn recibir_scancode(scancode: u8) { + // 1. Rastrear la tecla Alt — el modificador de los mandos del sistema. Se + // consume: el modificador nunca se difunde a una app. + match scancode { + ALT_MAKE => { + ALT_PULSADO.store(true, Ordering::Relaxed); + return; + } + ALT_BREAK => { + ALT_PULSADO.store(false, Ordering::Relaxed); + return; + } + _ => {} + } + + // 2. Con Alt pulsada, los make codes son MANDOS del compositor. Se traducen + // a una orden en la cola lock-free y se consumen — jamas llegan a una app. + if ALT_PULSADO.load(Ordering::Relaxed) { + match scancode { + ESPACIO => compositor::solicitar(Mando::CiclarLayout), + TECLA_J => compositor::solicitar(Mando::FocoSiguiente), + TECLA_K => compositor::solicitar(Mando::FocoAnterior), + _ => {} + } + return; + } + + // 3. Tecla ordinaria: se entrega SOLO a la app que tiene el foco. El foco + // es un indice atomico; el censo, un vector indexado por `indice_app`. + if let Some(censo) = CANALES.get() { + if let Some(Some(canal)) = censo.lock().get(compositor::foco()) { + // Si el 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/compositor.rs b/renaser/kernel/src/compositor.rs index ac51508..8921ab9 100644 --- a/renaser/kernel/src/compositor.rs +++ b/renaser/kernel/src/compositor.rs @@ -1,41 +1,323 @@ // ============================================================================= // renaser :: kernel/src/compositor.rs — Fase 8 :: el compositor teselante // ----------------------------------------------------------------------------- -// Hasta la Fase 7, cada app llevaba su region escrita a mano en el manifiesto: -// coordenadas fijas, una composicion rigida. La Fase 8 entrega esa decision a -// un COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA. +// El kernel no coloca las ventanas a mano: las TESELA. El motor es +// `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland +// de brahman—, enlazado por `path` cruzando la frontera de workspace. // -// El motor de teselado es `mirada-layout` — el mismo nucleo `no_std` que -// ordena las ventanas del compositor Wayland de brahman. Cruza la frontera de -// workspace y se enlaza aqui sin una linea de codigo nueva: geometria pura, -// determinista, la misma en Linux y en el bare-metal de renaser. +// FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro +// de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo +// fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en +// caliente —o mover el foco— y el kernel recompone cada ventana en su marco +// nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva +// su imagen intacta a traves de cualquier reordenacion. // -// Cada app conserva su tamaño NATURAL —el lienzo que sabe pintar, fijo—; el -// compositor decide DONDE va ese lienzo. El kernel centra el fotograma natural -// de la app dentro del marco teselado. Asi el compositor reordena la pantalla -// sin que ninguna app cambie una sola instruccion. +// EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas +// cooperativas (el `tick` de una app, la tarea del compositor): el manejador +// de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por +// un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el +// estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ +// pudiera disputar a una tarea cooperativa. // ============================================================================= +use core::sync::atomic::{AtomicUsize, Ordering}; + +use alloc::vec; use alloc::vec::Vec; +use crossbeam_queue::ArrayQueue; use mirada_layout::{tile, LayoutMode, LayoutParams, Rect}; +use spin::{Mutex, Once}; -use crate::grafico::RegionPantalla; +use crate::grafico::{Color, RegionPantalla}; /// Altura del strip superior reservado a la consola; las apps teselan debajo. /// La consola conserva ahi su registro de arranque completo —seis lineas, /// hasta la sonda asincrona de disco— legible sobre el teselado. const FRANJA_CONSOLA: usize = 296; -/// El modo de teselado del compositor. Fijo por ahora — la Fase 8b lo hara -/// conmutable en caliente desde el teclado, recorriendo los siete modos que -/// `mirada-layout` ofrece. -const MODO: LayoutMode = LayoutMode::MasterStack; +/// El modo de teselado con que arranca el escritorio. El teclado lo cicla. +const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack; -/// Margen entre ventanas teseladas, en pixeles — el aire que separa un marco -/// de sus vecinos. +/// Margen entre ventanas teseladas, en pixeles. const MARGEN: i32 = 14; +/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto. +const CAPACIDAD_MANDOS: usize = 32; + +/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo +/// atiende la tarea del compositor desde el reactor cooperativo. +#[derive(Clone, Copy)] +pub enum Mando { + /// Ciclar al siguiente modo de teselado de `mirada-layout`. + CiclarLayout, + /// Mover el foco a la siguiente ventana viva. + FocoSiguiente, + /// Mover el foco a la ventana viva anterior. + FocoAnterior, +} + +/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma. +struct Ventana { + /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo. + natural_ancho: usize, + natural_alto: usize, + /// El marco teselado actual — donde la app vive en pantalla. Cambia con + /// cada re-teselado. + marco: RegionPantalla, + /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su + /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto × + /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El + /// re-teselado recompone la ventana desde aqui, sin molestar a la app. + cache: Vec, + /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es + /// solo ceros y no se recompone. + pintada: bool, + /// Si el kernel desalojo la app, el color de su baliza. `None` mientras + /// vive; `Some(color)` la marca como muerta y la excluye del foco. + baliza: Option, +} + +/// El escritorio: el registro de todas las ventanas y el modo de teselado. +/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1. +struct Escritorio { + modo: LayoutMode, + ancho: usize, + alto: usize, + ventanas: Vec, +} + +/// El escritorio global. Se funda una sola vez, en el arranque. +static ESCRITORIO: Once> = Once::new(); + +/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`— +/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no +/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea. +static FOCO: AtomicUsize = AtomicUsize::new(0); + +/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del +/// teclado (lock-free, segura en contexto de interrupcion); la tarea del +/// compositor las drena desde el reactor cooperativo. +static MANDOS: Once> = Once::new(); + +// ============================================================================= +// Fundacion y consulta — el arranque +// ============================================================================= + +/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial +/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el +/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto. +pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) { + MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS)); + + let marcos = teselar(naturales.len(), ancho, alto, MODO_INICIAL); + let mut ventanas = Vec::with_capacity(naturales.len()); + for (i, &(nat_ancho, nat_alto)) in naturales.iter().enumerate() { + ventanas.push(Ventana { + natural_ancho: nat_ancho, + natural_alto: nat_alto, + marco: marcos[i], + // La cache: reservada UNA vez, acotada al lienzo natural. + cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)], + pintada: false, + baliza: None, + }); + } + + ESCRITORIO.call_once(|| { + Mutex::new(Escritorio { + modo: MODO_INICIAL, + ancho, + alto, + ventanas, + }) + }); +} + +/// Pinta el escenario inicial del compositor: el area de apps y sus marcos +/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps. +pub fn componer_escenario() { + let Some(escritorio) = ESCRITORIO.get() else { + return; + }; + let escritorio = escritorio.lock(); + let area = area_apps(escritorio.ancho, escritorio.alto); + let marcos: Vec = escritorio.ventanas.iter().map(|v| v.marco).collect(); + crate::consola::pintar_escenario(area, &marcos); +} + +/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar +/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno. +pub fn foco() -> usize { + FOCO.load(Ordering::Relaxed) +} + +/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una +/// cola lock-free es seguro en contexto de interrupcion. +pub fn solicitar(mando: Mando) { + if let Some(mandos) = MANDOS.get() { + // Si la cola se desborda, el mando se pierde en silencio: mas vale + // perder una pulsacion que arriesgar un panico dentro de una IRQ. + let _ = mandos.push(mando); + } +} + +// ============================================================================= +// El fotograma de una app — cache y composicion +// ============================================================================= + +/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el +/// kernel asume la persistencia visual— y lo compone, centrado, en su marco. +/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo. +pub fn presentar_fotograma(indice: usize, datos: &[u8]) { + let Some(escritorio) = ESCRITORIO.get() else { + return; + }; + let (marco, nat_ancho, nat_alto) = { + let mut escritorio = escritorio.lock(); + let Some(ventana) = escritorio.ventanas.get_mut(indice) else { + return; + }; + // Cachear el fotograma. El destino esta acotado al lienzo natural; se + // copia el minimo de ambas longitudes — jamas se desborda la cache. + let n = ventana.cache.len().min(datos.len()); + ventana.cache[..n].copy_from_slice(&datos[..n]); + ventana.pintada = true; + (ventana.marco, ventana.natural_ancho, ventana.natural_alto) + }; + let enfocada = FOCO.load(Ordering::Relaxed) == indice; + crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada); +} + +/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza. +/// Desde aqui queda excluida del foco — el teclado la salta. +pub fn desalojar(indice: usize, color: Color) { + let Some(escritorio) = ESCRITORIO.get() else { + return; + }; + let marco = { + let mut escritorio = escritorio.lock(); + let Some(ventana) = escritorio.ventanas.get_mut(indice) else { + return; + }; + ventana.baliza = Some(color); + ventana.marco + }; + let enfocada = FOCO.load(Ordering::Relaxed) == indice; + crate::consola::pintar_desalojo(marco, color, enfocada); +} + +// ============================================================================= +// Los mandos del teclado — el escritorio interactivo +// ============================================================================= + +/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor +/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es +/// seguro bloquear el `ESCRITORIO` y la consola. +pub fn atender_mandos() { + let Some(mandos) = MANDOS.get() else { + return; + }; + while let Some(mando) = mandos.pop() { + match mando { + Mando::CiclarLayout => ciclar_layout(), + Mando::FocoSiguiente => mover_foco(true), + Mando::FocoAnterior => mover_foco(false), + } + } +} + +/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las +/// ventanas y recompone el escritorio entero desde las caches de respaldo. +fn ciclar_layout() { + let Some(escritorio) = ESCRITORIO.get() else { + return; + }; + let mut escritorio = escritorio.lock(); + escritorio.modo = escritorio.modo.next(); + + let marcos = teselar( + escritorio.ventanas.len(), + escritorio.ancho, + escritorio.alto, + escritorio.modo, + ); + for (ventana, marco) in escritorio.ventanas.iter_mut().zip(marcos) { + ventana.marco = marco; + } + redibujar_todo(&escritorio); +} + +/// Mueve el foco a la siguiente ventana VIVA —saltando las desalojadas—; tras +/// el salto, redibuja la ventana que pierde el foco y la que lo gana, para que +/// el borde de cada una cambie de color. +fn mover_foco(adelante: bool) { + let Some(escritorio) = ESCRITORIO.get() else { + return; + }; + let escritorio = escritorio.lock(); + let n = escritorio.ventanas.len(); + if n == 0 { + return; + } + let anterior = FOCO.load(Ordering::Relaxed).min(n - 1); + + // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras + // `n` pasos se vuelve al punto de partida y el foco no cambia. + let mut nuevo = anterior; + for _ in 0..n { + nuevo = if adelante { + (nuevo + 1) % n + } else { + (nuevo + n - 1) % n + }; + if escritorio.ventanas[nuevo].baliza.is_none() { + break; + } + } + FOCO.store(nuevo, Ordering::Relaxed); + + redibujar_ventana(&escritorio.ventanas[anterior], false); + redibujar_ventana(&escritorio.ventanas[nuevo], true); +} + +/// Recompone el escritorio entero: repinta el escenario —area y paneles— con +/// los marcos nuevos y, sobre el, cada ventana desde su cache de respaldo. +fn redibujar_todo(escritorio: &Escritorio) { + let area = area_apps(escritorio.ancho, escritorio.alto); + let marcos: Vec = escritorio.ventanas.iter().map(|v| v.marco).collect(); + crate::consola::pintar_escenario(area, &marcos); + + let foco = FOCO.load(Ordering::Relaxed); + for (i, ventana) in escritorio.ventanas.iter().enumerate() { + redibujar_ventana(ventana, i == foco); + } +} + +/// Redibuja UNA ventana en su marco actual: si fue desalojada, su baliza; si ya +/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel +/// del escenario ya esta puesto—. +fn redibujar_ventana(ventana: &Ventana, enfocada: bool) { + match ventana.baliza { + Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada), + None => { + if ventana.pintada { + crate::consola::volcar_marco( + ventana.marco, + ventana.natural_ancho, + ventana.natural_alto, + &ventana.cache, + enfocada, + ); + } + } + } +} + +// ============================================================================= +// Teselado — la geometria pura de `mirada-layout` +// ============================================================================= + /// El area de pantalla que el compositor tesela: toda la pantalla menos la /// franja de la consola en la cima. pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla { @@ -47,11 +329,10 @@ pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla } } -/// Tesela el area de apps en `n` marcos —uno por ventana, en el orden de las -/// apps del manifiesto— con el algoritmo de `mirada-layout`. El vector -/// resultante tiene exactamente `n` elementos. -pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec { - let area = area_apps(ancho_pantalla, alto_pantalla); +/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante +/// tiene exactamente `n` elementos, en el orden de las apps del manifiesto. +fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec { + let area = area_apps(ancho, alto); let pantalla = Rect::new( area.x as i32, area.y as i32, @@ -59,7 +340,7 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec Vec RegionPantalla { RegionPantalla { x: r.x.max(0) as usize, diff --git a/renaser/kernel/src/consola.rs b/renaser/kernel/src/consola.rs index 8ed5886..cd29ec0 100644 --- a/renaser/kernel/src/consola.rs +++ b/renaser/kernel/src/consola.rs @@ -130,6 +130,7 @@ impl Consola { nat_ancho: usize, nat_alto: usize, datos: &[u8], + enfocada: bool, ) { if nat_ancho == 0 || nat_alto == 0 { return; @@ -165,17 +166,47 @@ impl Consola { self.lienzo.pixeles[y * self.lienzo.ancho + x] = codificar(self.lienzo.formato, color); } + // El borde del compositor: delata, de un vistazo, quien tiene el foco. + self.dibujar_borde(marco, enfocada); self.presentar(); } - /// Inunda una region entera con un color plano y la presenta. Es la baliza - /// de desalojo: cuando una aplicacion falla, su marco se tatua de purpura. - fn pintar_region(&mut self, region: RegionPantalla, color: Color) { + /// Inunda una region entera con un color plano —la baliza de desalojo: una + /// app que falla tatua su marco de purpura— y le traza su borde de foco. + fn pintar_region(&mut self, region: RegionPantalla, color: Color, enfocada: bool) { self.lienzo .rellenar_rect(region.x, region.y, region.ancho, region.alto, color); + self.dibujar_borde(region, enfocada); self.presentar(); } + /// Traza un borde de 3 px alrededor de un marco: indigo brillante si la + /// ventana tiene el foco del compositor, gris mate si no (Fase 8c). + fn dibujar_borde(&mut self, marco: RegionPantalla, enfocada: bool) { + const GROSOR: usize = 3; + let color = if enfocada { Color::FOCO } else { Color::SIN_FOCO }; + // Lados superior e inferior. + self.lienzo + .rellenar_rect(marco.x, marco.y, marco.ancho, GROSOR, color); + self.lienzo.rellenar_rect( + marco.x, + marco.y + marco.alto.saturating_sub(GROSOR), + marco.ancho, + GROSOR, + color, + ); + // Lados izquierdo y derecho. + self.lienzo + .rellenar_rect(marco.x, marco.y, GROSOR, marco.alto, color); + self.lienzo.rellenar_rect( + marco.x + marco.ancho.saturating_sub(GROSOR), + marco.y, + GROSOR, + marco.alto, + color, + ); + } + /// Pinta el escenario del compositor (Fase 8): inunda el area de apps con /// el reposo del lienzo —borrando cuanto hubiera debajo— y, sobre ella, /// tiñe cada marco teselado con el color de panel. Asi el teselado se ve @@ -205,36 +236,38 @@ impl Consola { /// asincronas y las capacidades del userspace escriben en ella tras su `Mutex`. pub(crate) static CONSOLA: Once> = 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— centrado en -/// el marco que el compositor asigno a esa aplicacion. `nat_ancho`/`nat_alto` -/// son el tamaño natural del lienzo de la app. -pub(crate) fn volcar_marco_wasm( +/// Compone un fotograma del userspace —ya cacheado por el compositor— centrado +/// en su marco teselado, con su borde de foco. La invoca `compositor` al +/// recibir un `sys_render_frame` y al recomponer el escritorio tras un mando. +pub(crate) fn volcar_marco( marco: RegionPantalla, nat_ancho: usize, nat_alto: usize, datos: &[u8], + enfocada: bool, ) { if let Some(consola) = CONSOLA.get() { - consola.lock().volcar_marco(marco, nat_ancho, nat_alto, datos); + consola + .lock() + .volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada); } } /// Pinta el escenario del compositor (Fase 8): el area de apps y, sobre ella, -/// cada marco teselado. Se invoca una vez, en el arranque, tras teselar. +/// cada marco teselado. La invoca `compositor` al arrancar y al re-teselar. pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) { if let Some(consola) = CONSOLA.get() { consola.lock().pintar_escenario(area, marcos); } } -/// 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) { +/// Tatua la baliza de desalojo sobre el marco de una aplicacion que el kernel +/// ha dado por terminada, con su borde de foco. 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(marco: RegionPantalla, color: Color, enfocada: bool) { if let Some(consola) = CONSOLA.get() { - consola.lock().pintar_region(region, color); + consola.lock().pintar_region(marco, color, enfocada); } } diff --git a/renaser/kernel/src/grafico.rs b/renaser/kernel/src/grafico.rs index 2edecb5..e04e07c 100644 --- a/renaser/kernel/src/grafico.rs +++ b/renaser/kernel/src/grafico.rs @@ -52,6 +52,22 @@ impl Color { b: 0x30, }; + /// Borde de la ventana ENFOCADA (Fase 8c): un indigo brillante. Delata, de + /// un vistazo, a quien recibe el teclado. + pub(crate) const FOCO: Color = Color { + r: 0x4B, + g: 0x00, + b: 0x82, + }; + + /// Borde de una ventana sin foco (Fase 8c): un gris mate, discreto — marca + /// el marco sin reclamar la atencion. + pub(crate) const SIN_FOCO: Color = Color { + r: 0x3A, + g: 0x40, + b: 0x4E, + }; + /// Alerta de colapso: un rojo saturado, imposible de ignorar. pub(crate) const ALERTA: Color = Color { r: 0xD4, diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs index 4f63a8b..2a96c6a 100644 --- a/renaser/kernel/src/main.rs +++ b/renaser/kernel/src/main.rs @@ -60,16 +60,16 @@ mod sync; mod texto; mod wasm; -// Reexportaciones para que los submodulos conserven rutas `crate::` estables. -pub(crate) use consola::volcar_marco_wasm; +// Reexportacion para que los submodulos conserven rutas `crate::` estables. pub(crate) use sync::CeldaSync; +use alloc::vec::Vec; + 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, + codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, ALTO_MAX, ANCHO_MAX, }; /// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU. @@ -94,20 +94,32 @@ pub(crate) fn detener() -> ! { /// 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. +/// DESALOJA: el compositor tatua su ventana con la baliza 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.marco(), falla.color_baliza()); + // o aborto, amarillo si reviento su techo de memoria. El compositor + // la pinta en el marco actual de la ventana y la marca como muerta. + compositor::desalojar(app.indice(), falla.color_baliza()); return; } } } +/// FASE 8 :: la tarea del compositor. En cada fotograma drena la cola de mandos +/// que el teclado dejo —ciclar el modo de teselado, mover el foco— y los +/// aplica. Corre en el reactor cooperativo: el unico contexto donde es seguro +/// re-teselar el escritorio y recomponer el lienzo desde las caches. +async fn tarea_compositor() { + loop { + async_system::reloj::EsperaFrame::nueva().await; + compositor::atender_mandos(); + } +} + /// 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 @@ -132,19 +144,13 @@ async fn tarea_sonda_disco() { } /// Da vida a una aplicacion del userspace a partir de su `EntradaApp` del -/// manifiesto: recupera su bytecode del grafo, la carga en el `marco` que el -/// compositor le teselo y la despacha como tarea cooperativa del reactor. Si -/// el bytecode falta, esta corrupto, o la carga fracasa, se salda pintando el -/// marco con la baliza de desalojo — el kernel no se inmuta y sigue con las -/// demas. -fn encender_app( - ejecutor: &mut Executor, - indice: usize, - entrada: &manifiesto::EntradaApp, - marco: RegionPantalla, -) { +/// manifiesto: recupera su bytecode del grafo, lo carga en la ventana `indice` +/// del escritorio del compositor y despacha la app como tarea cooperativa del +/// reactor. Si el bytecode falta, esta corrupto, o la carga fracasa, el +/// compositor desaloja esa ventana — el kernel sigue con las demas. +fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::EntradaApp) { // El tamaño NATURAL del lienzo de la app —lo que sabe pintar, fijo— lo - // dicta su `EntradaApp`; el compositor le asigno `marco` como ventana. + // dicta su `EntradaApp`; el compositor decide en que marco lo coloca. let natural = manifiesto::region(entrada); // Recuperar el bytecode del grafo. `recuperar` recomputa el hash del // objeto y verifica su integridad: un bytecode corrupto se delata aqui @@ -152,22 +158,21 @@ fn encender_app( let bytecode = match almacen::recuperar(&entrada.bytecode) { Ok(Some(objeto)) => objeto.datos, _ => { - consola::pintar_desalojo(marco, Color::DESALOJO); + compositor::desalojar(indice, Color::DESALOJO); return; } }; - // `indice` es la identidad de la app en el manifiesto: las capacidades de - // estado persistido (Fase 7c) la usan para hallar SU ranura `estado`. + // `indice` es la identidad de la app: su ventana en el escritorio del + // compositor y su ranura de estado persistido (Fase 7c). match wasm::AplicacionWasm::cargar( &bytecode, - marco, natural.ancho, natural.alto, entrada.techo_memoria as usize, indice, ) { Ok(app) => ejecutor.spawn(tarea_aplicacion(app)), - Err(_) => consola::pintar_desalojo(marco, Color::DESALOJO), + Err(_) => compositor::desalojar(indice, Color::DESALOJO), } } @@ -218,18 +223,25 @@ fn cargar_userspace(ejecutor: &mut Executor, ancho_pantalla: usize, alto_pantall // consulta lee del manifiesto vivo. manifiesto::instalar(m.clone()); - // FASE 8 :: el compositor tesela el area de apps — un marco por - // ventana, calculado por `mirada-layout`. Se pinta el escenario (el - // area y sus marcos) antes de encender las apps: el teselado se ve - // aunque alguna app no llegue siquiera a pintar su primer fotograma. - let marcos = compositor::disponer(m.apps.len(), ancho_pantalla, alto_pantalla); - consola::pintar_escenario( - compositor::area_apps(ancho_pantalla, alto_pantalla), - &marcos, - ); - for (indice, (entrada, marco)) in m.apps.iter().zip(marcos).enumerate() { - encender_app(ejecutor, indice, entrada, marco); + // FASE 8 :: fundar el escritorio del compositor — una ventana por app, + // con su cache de respaldo y su marco teselado por `mirada-layout`— y + // pintar el escenario antes de encender las apps: el teselado se ve + // aunque alguna app no llegue a pintar su primer fotograma. + let naturales: Vec<(usize, usize)> = m + .apps + .iter() + .map(|e| (e.region_ancho as usize, e.region_alto as usize)) + .collect(); + compositor::fundar(ancho_pantalla, alto_pantalla, &naturales); + compositor::componer_escenario(); + + for (indice, entrada) in m.apps.iter().enumerate() { + encender_app(ejecutor, indice, entrada); } + + // La tarea del compositor: atiende los mandos del teclado —ciclar el + // teselado, mover el foco— en cada fotograma del reactor. + ejecutor.spawn(tarea_compositor()); } } diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs index b560cec..d7a7792 100644 --- a/renaser/kernel/src/wasm/env.rs +++ b/renaser/kernel/src/wasm/env.rs @@ -29,18 +29,15 @@ 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 { - /// El marco que el compositor (Fase 8) asigno a la app — el rectangulo de - /// pantalla donde vive. El kernel centra en el el fotograma de la app. - pub(crate) marco: RegionPantalla, /// El tamaño natural del lienzo de la app, en pixeles. El fotograma que /// entrega `sys_render_frame` mide exactamente `natural_ancho × natural_alto`; - /// el compositor lo coloca, sin deformarlo, dentro del `marco`. + /// el compositor lo cachea y lo compone, sin deformarlo, en el marco que el + /// teselado le asigno. pub(crate) natural_ancho: usize, pub(crate) natural_alto: usize, /// El canal de teclado propio de la aplicacion. @@ -48,9 +45,9 @@ pub(crate) struct ContextoCapacidades { /// 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, - /// El indice de esta app en el Manifiesto de Genesis — su identidad. Las - /// capacidades de estado (Fase 7c) lo usan para hallar la `EntradaApp` - /// correcta: cada app persiste en SU ranura, jamas en la de otra. + /// El indice de esta app — su identidad. La usan las capacidades de estado + /// (Fase 7c) para hallar su `EntradaApp` del manifiesto, y el compositor + /// (Fase 8) para hallar su ventana en el escritorio: jamas la de otra. pub(crate) indice_app: usize, } @@ -98,7 +95,7 @@ pub(crate) fn enlazar_capacidades( "renaser", "sys_render_frame", |caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> { - let marco = caller.data().marco; + let indice = caller.data().indice_app; let nat_ancho = caller.data().natural_ancho; let nat_alto = caller.data().natural_alto; @@ -124,9 +121,10 @@ pub(crate) fn enlazar_capacidades( "WASM :: sys_render_frame desbordo la memoria lineal del modulo", )?; - // Limites verificados: el compositor centra el fotograma natural - // de la app dentro del marco que el teselado le asigno. - crate::volcar_marco_wasm(marco, nat_ancho, nat_alto, fotograma); + // Limites verificados: el compositor cachea el fotograma —para + // poder recomponerlo si el escritorio se re-tesela— y lo compone, + // centrado, en el marco que el teselado asigno a esta app. + crate::compositor::presentar_fotograma(indice, fotograma); Ok(()) }, )?; diff --git a/renaser/kernel/src/wasm/mod.rs b/renaser/kernel/src/wasm/mod.rs index b6c3bf6..f706bac 100644 --- a/renaser/kernel/src/wasm/mod.rs +++ b/renaser/kernel/src/wasm/mod.rs @@ -20,7 +20,7 @@ use wasmi::{ CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc, }; -use crate::grafico::{Color, RegionPantalla}; +use crate::grafico::Color; use env::ContextoCapacidades; /// Combustible concedido a `init`. Cubre con holgura el pintado inicial del @@ -64,34 +64,28 @@ impl FallaApp { /// 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. + /// globales y el contexto de capacidades con su identidad e indice. almacen: Store, /// 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<(), ()>, - /// El marco que el compositor asigno a la app — su ventana en pantalla, y - /// donde se tatua su baliza de desalojo si llega a fallar. - marco: 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. + /// Carga, valida, instancia y arranca una aplicacion WASM aislada. 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. + /// El 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. /// - /// `marco` es el rectangulo que el compositor (Fase 8) asigno a la app; - /// `natural_ancho`/`natural_alto`, el tamaño de su lienzo. `techo_memoria` - /// es su cuota de memoria lineal —la dicta su `EntradaApp` del manifiesto—, - /// e `indice_app` su posicion en el: su identidad para las capacidades de - /// estado persistido (Fase 7c). + /// `natural_ancho`/`natural_alto` son el tamaño del lienzo de la app; + /// `techo_memoria`, su cuota de memoria lineal —la dicta su `EntradaApp` del + /// manifiesto—; e `indice_app`, su identidad: la posicion con que el + /// compositor halla su ventana y las capacidades de estado su ranura. pub fn cargar( bytecode: &[u8], - marco: RegionPantalla, natural_ancho: usize, natural_alto: usize, techo_memoria: usize, @@ -108,10 +102,10 @@ impl AplicacionWasm { // 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. + // 3. El almacen, con el contexto de capacidades de ESTA app: su lienzo + // natural, su canal de teclado y su techo de memoria. El canal se + // crea ahora pero se inscribe en el censo 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) @@ -122,7 +116,6 @@ impl AplicacionWasm { let mut almacen = Store::new( &motor, ContextoCapacidades { - marco, natural_ancho, natural_alto, canal, @@ -161,14 +154,11 @@ impl AplicacionWasm { .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); + // en el censo de la IRQ1, en la ranura de su `indice_app`: desde + // aqui recibe las teclas cuando el compositor le da el foco. + crate::async_system::teclado::registrar_canal(indice_app, &almacen.data().canal); - Ok(AplicacionWasm { - almacen, - func_tick, - marco, - }) + Ok(AplicacionWasm { almacen, func_tick }) } /// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de @@ -194,10 +184,11 @@ impl AplicacionWasm { } } - /// El marco de pantalla que el compositor asigno a la aplicacion — donde - /// se tatua su baliza si el kernel llega a desalojarla. - pub fn marco(&self) -> RegionPantalla { - self.marco + /// El indice de la aplicacion — su identidad en el escritorio del + /// compositor. Lo usa la tarea de la app para decirle al compositor que + /// ventana desalojar si la app llega a fallar. + pub fn indice(&self) -> usize { + self.almacen.data().indice_app } } @@ -207,6 +198,6 @@ impl AplicacionWasm { /// 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); + crate::async_system::teclado::cerrar_canal(self.almacen.data().indice_app); } }