feat(renaser): Fases 8b y 8c — el escritorio interactivo

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 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 19:19:21 +00:00
parent e94023d8af
commit 5c462e6d30
12 changed files with 676 additions and 177 deletions
+59
View File
@@ -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 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 entero con la baliza. El registro de arranque permanece legible en la franja
superior de la consola. 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<Option<CanalTeclado>>`): 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.
+5 -5
View File
@@ -73,11 +73,11 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
## Estado ## Estado
Fases 1 a 5, 6.0, 6.1, 6.2 y la Fase 7 COMPLETA —el userspace nace del grafo Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de
de objetos: Manifiesto de Génesis (7a), imagen sembrada por `boot` (7b) y objetos— y la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
persistencia inter-sesión por-app (7c)—. Fase 8 en curso: 8a hecha —el con `mirada-layout` (8a), `Alt+Espacio` cicla el layout (8b), foco con
compositor tesela las ventanas con `mirada-layout`—. Todo verificado en QEMU. `Alt+J`/`Alt+K` y enrutamiento selectivo del teclado (8c)—. Todo verificado en
Ver `ROADMAP.md`. QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo ## Flujo de trabajo
+30
View File
@@ -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 quien lo centra con cuidado en el espacio que le tocó. Nadie tuvo que cambiar
para vivir mejor repartido. 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.* *El diario continúa. La próxima página la escribirá la próxima jornada.*
+20 -12
View File
@@ -29,21 +29,29 @@ bare-metal.
- `EntradaApp.region_ancho/alto` pasa a significar el tamaño NATURAL del lienzo - `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. 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` - `Alt+Espacio` cicla en caliente los siete modos de teselado de
(`MasterStack`, `CenteredMaster`, `Spiral`, `Grid`, `Columns`, `Rows`, `mirada-layout` (`MasterStack`, `CenteredMaster`, `Spiral`, …).
`Monocle`). - Re-teselar exige re-componer. La solución: el kernel guarda una CACHÉ del
- Re-teselar exige re-componer: las apps que sólo pintan en `init` necesitan último fotograma de cada app —acotada al lienzo natural, reservada una sola
una señal de redibujado, o el kernel debe conservar su último fotograma para vez— y recompone desde ella. Una app que sólo pintó en su `init` conserva su
recomponerlo en el marco nuevo. 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 - Una ventana ENFOCADA, con borde índigo brillante; las demás, borde gris
ventanas en vivo. mate. `Alt+J` / `Alt+K` pasean el foco entre las ventanas vivas.
- Una ventana enfocada, resaltada; el teclado mueve el foco y promueve apps al - El teclado deja de difundir a ciegas: una tecla ordinaria se entrega sólo a
área maestra. 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 ## Estructura de archivos
+10 -6
View File
@@ -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` / y la app retoma donde quedó. Capacidades `sys_estado_cargar` /
`sys_estado_guardar`; el kernel custodia un manifiesto VIVO y mutable. `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 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 `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. 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 `region_x/y` del manifiesto quedan vestigiales — la posición la decide el
compositor. compositor.
- **8b — teselado interactivo (pendiente).** El teclado cicla en caliente los - **8b — teselado interactivo (completada).** `Alt+Espacio` cicla los modos de
siete modos de teselado de `mirada-layout`. teselado en caliente. El kernel cachea el último fotograma de cada app y
- **8c — foco y `Workspace` (pendiente).** Foco, orden-Z y alta/baja de recompone desde la caché: las apps estáticas sobreviven al re-teselado sin
ventanas en vivo, con el `Workspace` de `mirada-layout`. 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 ## Principios que persisten entre fases
+97 -29
View File
@@ -1,16 +1,28 @@
// ============================================================================= // =============================================================================
// renaser :: async_system/teclado.rs — el canal de scancodes del teclado // 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, // lock-free, seguras frente a interrupciones. Los consumidores —las apps WASM,
// via la capacidad `sys_get_scancode`— las drenan sin bloquear. // via la capacidad `sys_get_scancode`— las drenan sin bloquear.
// //
// FASE 5 :: con varias apps concurrentes, una sola cola compartida no sirve: // FASE 5 :: cada app abre su PROPIO canal; la primera en sondear no le roba la
// la primera en sondear le robaria la pulsacion a las demas. Por eso cada // pulsacion a las demas.
// aplicacion abre su PROPIO canal y la IRQ1 DIFUNDE cada scancode a todos — //
// cada app recibe su copia integra del flujo de entrada. // 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::sync::Arc;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -18,16 +30,33 @@ use crossbeam_queue::ArrayQueue;
use spin::{Mutex, Once}; use spin::{Mutex, Once};
use x86_64::instructions::interrupts; use x86_64::instructions::interrupts;
use crate::compositor::{self, Mando};
/// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto. /// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto.
const CAPACIDAD_COLA: usize = 256; 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. /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
pub type CanalTeclado = Arc<ArrayQueue<u8>>; pub type CanalTeclado = Arc<ArrayQueue<u8>>;
/// Censo de canales — uno por aplicacion del userspace. El manejador de IRQ1 /// Censo de canales, INDEXADO por el `indice_app` de cada aplicacion. Una
/// difunde cada scancode a TODOS: asi cada app recibe su propia copia del /// ranura `None` es una app que no abrio canal o que fue desalojada. El
/// evento, sin que una le arrebate la pulsacion a otra. /// indexado estable permite que el foco —un simple indice— elija el canal.
static CANALES: Once<Mutex<Vec<CanalTeclado>>> = Once::new(); static CANALES: Once<Mutex<Vec<Option<CanalTeclado>>>> = 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 /// Funda el censo de canales del teclado. Requiere el heap ya activo; debe
/// invocarse una sola vez, antes de habilitar las interrupciones. /// invocarse una sola vez, antes de habilitar las interrupciones.
@@ -35,40 +64,79 @@ pub fn init() {
CANALES.call_once(|| Mutex::new(Vec::new())); 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. /// aplicacion reclama el suyo al empezar a cargarse.
pub fn crear_canal() -> CanalTeclado { pub fn crear_canal() -> CanalTeclado {
Arc::new(ArrayQueue::new(CAPACIDAD_COLA)) Arc::new(ArrayQueue::new(CAPACIDAD_COLA))
} }
/// Inscribe un canal en el censo de difusion. Desde este instante, la IRQ1 /// Inscribe el canal de la app `indice` en el censo. Desde este instante, una
/// empuja cada scancode tambien a este canal. Se invoca al final de la carga /// tecla ordinaria llega a esta app cuando tiene el foco. Se invoca al final de
/// de una app: una carga fallida no debe dejar canales huerfanos. /// la carga de una app: una carga fallida no debe dejar canales huerfanos.
pub fn registrar_canal(canal: &CanalTeclado) { pub fn registrar_canal(indice: usize, canal: &CanalTeclado) {
if let Some(censo) = CANALES.get() { if let Some(censo) = CANALES.get() {
// El cerrojo lo disputa el manejador de IRQ1: tomarlo con las // El cerrojo lo disputa el manejador de IRQ1: tomarlo con las
// interrupciones acalladas hace imposible el interbloqueo. // 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(|| { 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 /// Da de baja el canal de la app `indice`. Lo invoca el `Drop` de una
/// canales haya abiertos. Deliberadamente breve y libre de panicos: corre en /// aplicacion desalojada: la ranura queda en `None` y la IRQ deja de enrutarle
/// contexto de interrupcion. /// teclas, sin desplazar los indices de las demas.
pub fn recibir_scancode(scancode: u8) { pub fn cerrar_canal(indice: usize) {
if let Some(censo) = CANALES.get() { if let Some(censo) = CANALES.get() {
for canal in censo.lock().iter() { interrupts::without_interrupts(|| {
// Si un canal desborda, se descarta el scancode en silencio: mas 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. // vale perder una tecla que colapsar dentro de una interrupcion.
let _ = canal.push(scancode); let _ = canal.push(scancode);
} }
+307 -27
View File
@@ -1,41 +1,323 @@
// ============================================================================= // =============================================================================
// renaser :: kernel/src/compositor.rs — Fase 8 :: el compositor teselante // 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: // El kernel no coloca las ventanas a mano: las TESELA. El motor es
// coordenadas fijas, una composicion rigida. La Fase 8 entrega esa decision a // `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
// un COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA. // de brahman—, enlazado por `path` cruzando la frontera de workspace.
// //
// El motor de teselado es `mirada-layout` el mismo nucleo `no_std` que // FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
// ordena las ventanas del compositor Wayland de brahman. Cruza la frontera de // de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
// workspace y se enlaza aqui sin una linea de codigo nueva: geometria pura, // fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
// determinista, la misma en Linux y en el bare-metal de renaser. // 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 // EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
// compositor decide DONDE va ese lienzo. El kernel centra el fotograma natural // cooperativas (el `tick` de una app, la tarea del compositor): el manejador
// de la app dentro del marco teselado. Asi el compositor reordena la pantalla // de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
// sin que ninguna app cambie una sola instruccion. // 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 alloc::vec::Vec;
use crossbeam_queue::ArrayQueue;
use mirada_layout::{tile, LayoutMode, LayoutParams, Rect}; 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. /// Altura del strip superior reservado a la consola; las apps teselan debajo.
/// La consola conserva ahi su registro de arranque completo —seis lineas, /// La consola conserva ahi su registro de arranque completo —seis lineas,
/// hasta la sonda asincrona de disco— legible sobre el teselado. /// hasta la sonda asincrona de disco— legible sobre el teselado.
const FRANJA_CONSOLA: usize = 296; const FRANJA_CONSOLA: usize = 296;
/// El modo de teselado del compositor. Fijo por ahora — la Fase 8b lo hara /// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
/// conmutable en caliente desde el teclado, recorriendo los siete modos que const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
/// `mirada-layout` ofrece.
const MODO: LayoutMode = LayoutMode::MasterStack;
/// Margen entre ventanas teseladas, en pixeles — el aire que separa un marco /// Margen entre ventanas teseladas, en pixeles.
/// de sus vecinos.
const MARGEN: i32 = 14; 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<u8>,
/// ¿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<Color>,
}
/// 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<Ventana>,
}
/// El escritorio global. Se funda una sola vez, en el arranque.
static ESCRITORIO: Once<Mutex<Escritorio>> = 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<ArrayQueue<Mando>> = 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<RegionPantalla> = 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<RegionPantalla> = 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 /// El area de pantalla que el compositor tesela: toda la pantalla menos la
/// franja de la consola en la cima. /// franja de la consola en la cima.
pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla { 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 /// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
/// apps del manifiesto— con el algoritmo de `mirada-layout`. El vector /// tiene exactamente `n` elementos, en el orden de las apps del manifiesto.
/// resultante tiene exactamente `n` elementos. fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<RegionPantalla> { let area = area_apps(ancho, alto);
let area = area_apps(ancho_pantalla, alto_pantalla);
let pantalla = Rect::new( let pantalla = Rect::new(
area.x as i32, area.x as i32,
area.y as i32, area.y as i32,
@@ -59,7 +340,7 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<Re
area.alto as i32, area.alto as i32,
); );
let params = LayoutParams { let params = LayoutParams {
mode: MODO, mode: modo,
gap: MARGEN, gap: MARGEN,
..LayoutParams::default() ..LayoutParams::default()
}; };
@@ -69,9 +350,8 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<Re
.collect() .collect()
} }
/// Traduce un `Rect` de `mirada-layout` (`i32`, en teoria con signo) a la /// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
/// `RegionPantalla` del kernel (`usize`). Un rectangulo degenerado queda en /// kernel (`usize`). Un rectangulo degenerado queda en cero.
/// cero — el kernel no compondra nada en el.
fn rect_a_region(r: Rect) -> RegionPantalla { fn rect_a_region(r: Rect) -> RegionPantalla {
RegionPantalla { RegionPantalla {
x: r.x.max(0) as usize, x: r.x.max(0) as usize,
+50 -17
View File
@@ -130,6 +130,7 @@ impl Consola {
nat_ancho: usize, nat_ancho: usize,
nat_alto: usize, nat_alto: usize,
datos: &[u8], datos: &[u8],
enfocada: bool,
) { ) {
if nat_ancho == 0 || nat_alto == 0 { if nat_ancho == 0 || nat_alto == 0 {
return; return;
@@ -165,17 +166,47 @@ impl Consola {
self.lienzo.pixeles[y * self.lienzo.ancho + x] = self.lienzo.pixeles[y * self.lienzo.ancho + x] =
codificar(self.lienzo.formato, color); codificar(self.lienzo.formato, color);
} }
// El borde del compositor: delata, de un vistazo, quien tiene el foco.
self.dibujar_borde(marco, enfocada);
self.presentar(); self.presentar();
} }
/// Inunda una region entera con un color plano y la presenta. Es la baliza /// Inunda una region entera con un color plano la baliza de desalojo: una
/// de desalojo: cuando una aplicacion falla, su marco se tatua de purpura. /// app que falla tatua su marco de purpura— y le traza su borde de foco.
fn pintar_region(&mut self, region: RegionPantalla, color: Color) { fn pintar_region(&mut self, region: RegionPantalla, color: Color, enfocada: bool) {
self.lienzo self.lienzo
.rellenar_rect(region.x, region.y, region.ancho, region.alto, color); .rellenar_rect(region.x, region.y, region.ancho, region.alto, color);
self.dibujar_borde(region, enfocada);
self.presentar(); 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 /// Pinta el escenario del compositor (Fase 8): inunda el area de apps con
/// el reposo del lienzo —borrando cuanto hubiera debajo— y, sobre ella, /// 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 /// 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`. /// asincronas y las capacidades del userspace escriben en ella tras su `Mutex`.
pub(crate) static CONSOLA: Once<Mutex<Consola>> = Once::new(); pub(crate) static CONSOLA: Once<Mutex<Consola>> = Once::new();
/// Puerta del kernel para la capacidad `sys_render_frame` del userspace WASM: /// Compone un fotograma del userspace —ya cacheado por el compositor— centrado
/// compone sobre la consola global un fotograma —cuyos limites el host ya /// en su marco teselado, con su borde de foco. La invoca `compositor` al
/// verifico matematicamente contra la memoria lineal del modulo— centrado en /// recibir un `sys_render_frame` y al recomponer el escritorio tras un mando.
/// el marco que el compositor asigno a esa aplicacion. `nat_ancho`/`nat_alto` pub(crate) fn volcar_marco(
/// son el tamaño natural del lienzo de la app.
pub(crate) fn volcar_marco_wasm(
marco: RegionPantalla, marco: RegionPantalla,
nat_ancho: usize, nat_ancho: usize,
nat_alto: usize, nat_alto: usize,
datos: &[u8], datos: &[u8],
enfocada: bool,
) { ) {
if let Some(consola) = CONSOLA.get() { 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, /// 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]) { pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) {
if let Some(consola) = CONSOLA.get() { if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_escenario(area, marcos); consola.lock().pintar_escenario(area, marcos);
} }
} }
/// Tatua la baliza de desalojo sobre la region de una aplicacion que el kernel /// Tatua la baliza de desalojo sobre el marco de una aplicacion que el kernel
/// ha dado por terminada. El color delata la causa —purpura para una falla de /// ha dado por terminada, con su borde de foco. El color delata la causa
/// ejecucion o de combustible, amarillo palido para un desbordo de memoria—. Es /// —purpura para una falla de ejecucion o de combustible, amarillo palido para
/// una advertencia NO fatal: la app muere, el kernel y sus vecinas siguen vivos. /// un desbordo de memoria—. Es una advertencia NO fatal: la app muere, el
pub(crate) fn pintar_desalojo(region: RegionPantalla, color: Color) { /// kernel y sus vecinas siguen vivos.
pub(crate) fn pintar_desalojo(marco: RegionPantalla, color: Color, enfocada: bool) {
if let Some(consola) = CONSOLA.get() { if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_region(region, color); consola.lock().pintar_region(marco, color, enfocada);
} }
} }
+16
View File
@@ -52,6 +52,22 @@ impl Color {
b: 0x30, 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. /// Alerta de colapso: un rojo saturado, imposible de ignorar.
pub(crate) const ALERTA: Color = Color { pub(crate) const ALERTA: Color = Color {
r: 0xD4, r: 0xD4,
+47 -35
View File
@@ -60,16 +60,16 @@ mod sync;
mod texto; mod texto;
mod wasm; mod wasm;
// Reexportaciones para que los submodulos conserven rutas `crate::` estables. // Reexportacion para que los submodulos conserven rutas `crate::` estables.
pub(crate) use consola::volcar_marco_wasm;
pub(crate) use sync::CeldaSync; pub(crate) use sync::CeldaSync;
use alloc::vec::Vec;
use async_system::executor::Executor; use async_system::executor::Executor;
use baliza::BALIZA_PANICO; use baliza::BALIZA_PANICO;
use consola::{Consola, CONSOLA}; use consola::{Consola, CONSOLA};
use grafico::{ use grafico::{
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX, codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, ALTO_MAX, ANCHO_MAX,
ANCHO_MAX,
}; };
/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU. /// 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 /// 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 /// 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 /// 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. /// El ejecutor la retira del censo, su memoria se libera, el kernel sigue vivo.
async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) { async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
loop { loop {
async_system::reloj::EsperaFrame::nueva().await; async_system::reloj::EsperaFrame::nueva().await;
if let Err(falla) = app.tick() { if let Err(falla) = app.tick() {
// El color de la baliza delata la causa: purpura si agoto su tiempo // El color de la baliza delata la causa: purpura si agoto su tiempo
// o aborto, amarillo si reviento su techo de memoria. // o aborto, amarillo si reviento su techo de memoria. El compositor
consola::pintar_desalojo(app.marco(), falla.color_baliza()); // la pinta en el marco actual de la ventana y la marca como muerta.
compositor::desalojar(app.indice(), falla.color_baliza());
return; 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 /// 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 /// 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 /// 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 /// 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 /// manifiesto: recupera su bytecode del grafo, lo carga en la ventana `indice`
/// compositor le teselo y la despacha como tarea cooperativa del reactor. Si /// del escritorio del compositor y despacha la app como tarea cooperativa del
/// el bytecode falta, esta corrupto, o la carga fracasa, se salda pintando el /// reactor. Si el bytecode falta, esta corrupto, o la carga fracasa, el
/// marco con la baliza de desalojo — el kernel no se inmuta y sigue con las /// compositor desaloja esa ventana — el kernel sigue con las demas.
/// demas. fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::EntradaApp) {
fn encender_app(
ejecutor: &mut Executor,
indice: usize,
entrada: &manifiesto::EntradaApp,
marco: RegionPantalla,
) {
// El tamaño NATURAL del lienzo de la app —lo que sabe pintar, fijo— lo // 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); let natural = manifiesto::region(entrada);
// Recuperar el bytecode del grafo. `recuperar` recomputa el hash del // Recuperar el bytecode del grafo. `recuperar` recomputa el hash del
// objeto y verifica su integridad: un bytecode corrupto se delata aqui // 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) { let bytecode = match almacen::recuperar(&entrada.bytecode) {
Ok(Some(objeto)) => objeto.datos, Ok(Some(objeto)) => objeto.datos,
_ => { _ => {
consola::pintar_desalojo(marco, Color::DESALOJO); compositor::desalojar(indice, Color::DESALOJO);
return; return;
} }
}; };
// `indice` es la identidad de la app en el manifiesto: las capacidades de // `indice` es la identidad de la app: su ventana en el escritorio del
// estado persistido (Fase 7c) la usan para hallar SU ranura `estado`. // compositor y su ranura de estado persistido (Fase 7c).
match wasm::AplicacionWasm::cargar( match wasm::AplicacionWasm::cargar(
&bytecode, &bytecode,
marco,
natural.ancho, natural.ancho,
natural.alto, natural.alto,
entrada.techo_memoria as usize, entrada.techo_memoria as usize,
indice, indice,
) { ) {
Ok(app) => ejecutor.spawn(tarea_aplicacion(app)), 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. // consulta lee del manifiesto vivo.
manifiesto::instalar(m.clone()); manifiesto::instalar(m.clone());
// FASE 8 :: el compositor tesela el area de apps — un marco por // FASE 8 :: fundar el escritorio del compositor — una ventana por app,
// ventana, calculado por `mirada-layout`. Se pinta el escenario (el // con su cache de respaldo y su marco teselado por `mirada-layout`— y
// area y sus marcos) antes de encender las apps: el teselado se ve // pintar el escenario antes de encender las apps: el teselado se ve
// aunque alguna app no llegue siquiera a pintar su primer fotograma. // aunque alguna app no llegue a pintar su primer fotograma.
let marcos = compositor::disponer(m.apps.len(), ancho_pantalla, alto_pantalla); let naturales: Vec<(usize, usize)> = m
consola::pintar_escenario( .apps
compositor::area_apps(ancho_pantalla, alto_pantalla), .iter()
&marcos, .map(|e| (e.region_ancho as usize, e.region_alto as usize))
); .collect();
for (indice, (entrada, marco)) in m.apps.iter().zip(marcos).enumerate() { compositor::fundar(ancho_pantalla, alto_pantalla, &naturales);
encender_app(ejecutor, indice, entrada, marco); 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());
} }
} }
+10 -12
View File
@@ -29,18 +29,15 @@ use wasmi::{Caller, Error, Extern, Linker, Memory, StoreLimits};
use crate::almacen::Hash; use crate::almacen::Hash;
use crate::async_system::teclado::CanalTeclado; use crate::async_system::teclado::CanalTeclado;
use crate::grafico::RegionPantalla;
/// El estado del host adscrito al `Store` de una aplicacion: cuanto necesita /// 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, /// 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. /// su canal de teclado y sus cuotas de recursos. Dos apps jamas comparten nada.
pub(crate) struct ContextoCapacidades { 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 /// El tamaño natural del lienzo de la app, en pixeles. El fotograma que
/// entrega `sys_render_frame` mide exactamente `natural_ancho × natural_alto`; /// 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_ancho: usize,
pub(crate) natural_alto: usize, pub(crate) natural_alto: usize,
/// El canal de teclado propio de la aplicacion. /// 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. /// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
/// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`. /// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
pub(crate) limites: StoreLimits, pub(crate) limites: StoreLimits,
/// El indice de esta app en el Manifiesto de Genesis — su identidad. Las /// El indice de esta app — su identidad. La usan las capacidades de estado
/// capacidades de estado (Fase 7c) lo usan para hallar la `EntradaApp` /// (Fase 7c) para hallar su `EntradaApp` del manifiesto, y el compositor
/// correcta: cada app persiste en SU ranura, jamas en la de otra. /// (Fase 8) para hallar su ventana en el escritorio: jamas la de otra.
pub(crate) indice_app: usize, pub(crate) indice_app: usize,
} }
@@ -98,7 +95,7 @@ pub(crate) fn enlazar_capacidades(
"renaser", "renaser",
"sys_render_frame", "sys_render_frame",
|caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> { |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_ancho = caller.data().natural_ancho;
let nat_alto = caller.data().natural_alto; 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", "WASM :: sys_render_frame desbordo la memoria lineal del modulo",
)?; )?;
// Limites verificados: el compositor centra el fotograma natural // Limites verificados: el compositor cachea el fotograma —para
// de la app dentro del marco que el teselado le asigno. // poder recomponerlo si el escritorio se re-tesela— y lo compone,
crate::volcar_marco_wasm(marco, nat_ancho, nat_alto, fotograma); // centrado, en el marco que el teselado asigno a esta app.
crate::compositor::presentar_fotograma(indice, fotograma);
Ok(()) Ok(())
}, },
)?; )?;
+25 -34
View File
@@ -20,7 +20,7 @@ use wasmi::{
CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc, CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc,
}; };
use crate::grafico::{Color, RegionPantalla}; use crate::grafico::Color;
use env::ContextoCapacidades; use env::ContextoCapacidades;
/// Combustible concedido a `init`. Cubre con holgura el pintado inicial del /// 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`. /// aqui la instancia se conserva y el kernel la hace avanzar `tick` a `tick`.
pub struct AplicacionWasm { pub struct AplicacionWasm {
/// El almacen: todo el estado de ESTA instancia — su memoria lineal, sus /// 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<ContextoCapacidades>, almacen: Store<ContextoCapacidades>,
/// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos. /// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
/// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta, /// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
/// el handle de la `Instance` no aporta nada y no se retiene. /// el handle de la `Instance` no aporta nada y no se retiene.
func_tick: TypedFunc<(), ()>, 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 { impl AplicacionWasm {
/// Carga, valida, instancia y arranca una aplicacion WASM aislada, ligada a /// Carga, valida, instancia y arranca una aplicacion WASM aislada. Si algo
/// una region de pantalla. Si algo falla en el camino, se devuelve la falla /// falla en el camino, se devuelve la falla en lugar de incendiar el kernel.
/// en lugar de incendiar el kernel.
/// ///
/// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una /// El ABI del userspace exige dos exportaciones: `init` —invocada una sola
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por /// vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por el
/// el reactor en cada pulso del reloj. /// reactor en cada pulso del reloj.
/// ///
/// `marco` es el rectangulo que el compositor (Fase 8) asigno a la app; /// `natural_ancho`/`natural_alto` son el tamaño del lienzo de la app;
/// `natural_ancho`/`natural_alto`, el tamaño de su lienzo. `techo_memoria` /// `techo_memoria`, su cuota de memoria lineal —la dicta su `EntradaApp` del
/// es su cuota de memoria lineal —la dicta su `EntradaApp` del manifiesto—, /// manifiesto—; e `indice_app`, su identidad: la posicion con que el
/// e `indice_app` su posicion en el: su identidad para las capacidades de /// compositor halla su ventana y las capacidades de estado su ranura.
/// estado persistido (Fase 7c).
pub fn cargar( pub fn cargar(
bytecode: &[u8], bytecode: &[u8],
marco: RegionPantalla,
natural_ancho: usize, natural_ancho: usize,
natural_alto: usize, natural_alto: usize,
techo_memoria: usize, techo_memoria: usize,
@@ -108,10 +102,10 @@ impl AplicacionWasm {
// 2. Validar y traducir el modulo — ya instrumentado con fuel. // 2. Validar y traducir el modulo — ya instrumentado con fuel.
let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?; let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?;
// 3. El almacen, con el contexto de capacidades de ESTA app: su region // 3. El almacen, con el contexto de capacidades de ESTA app: su lienzo
// de pantalla, su canal de teclado y su techo de memoria. El canal // natural, su canal de teclado y su techo de memoria. El canal se
// se crea ahora pero se inscribe en la difusion de la IRQ1 al final, // crea ahora pero se inscribe en el censo de la IRQ1 al final, ya con
// ya con la app cargada: una carga fallida no deja canales huerfanos. // la app cargada: una carga fallida no deja canales huerfanos.
let canal = crate::async_system::teclado::crear_canal(); let canal = crate::async_system::teclado::crear_canal();
let limites = StoreLimitsBuilder::new() let limites = StoreLimitsBuilder::new()
.memory_size(techo_memoria) .memory_size(techo_memoria)
@@ -122,7 +116,6 @@ impl AplicacionWasm {
let mut almacen = Store::new( let mut almacen = Store::new(
&motor, &motor,
ContextoCapacidades { ContextoCapacidades {
marco,
natural_ancho, natural_ancho,
natural_alto, natural_alto,
canal, canal,
@@ -161,14 +154,11 @@ impl AplicacionWasm {
.map_err(|_| FallaApp::Carga)?; .map_err(|_| FallaApp::Carga)?;
// 8. Con la app ya cargada e instanciada, inscribir su canal de teclado // 8. Con la app ya cargada e instanciada, inscribir su canal de teclado
// en la difusion de la IRQ1: desde aqui recibe cada pulsacion. // en el censo de la IRQ1, en la ranura de su `indice_app`: desde
crate::async_system::teclado::registrar_canal(&almacen.data().canal); // aqui recibe las teclas cuando el compositor le da el foco.
crate::async_system::teclado::registrar_canal(indice_app, &almacen.data().canal);
Ok(AplicacionWasm { Ok(AplicacionWasm { almacen, func_tick })
almacen,
func_tick,
marco,
})
} }
/// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de /// 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 /// El indice de la aplicacion — su identidad en el escritorio del
/// se tatua su baliza si el kernel llega a desalojarla. /// compositor. Lo usa la tarea de la app para decirle al compositor que
pub fn marco(&self) -> RegionPantalla { /// ventana desalojar si la app llega a fallar.
self.marco 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. /// empujando scancodes a una cola muerta: una fuga lenta pero segura.
impl Drop for AplicacionWasm { impl Drop for AplicacionWasm {
fn drop(&mut self) { 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);
} }
} }