tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Sutichaykuna

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>

5c462e6d307addd58614a8c4495f4becda04e4b2 · ruwaq sergio · (5 killa ñawpaq) · tayta e94023d8a · kay commitpi sach'ata qhaway

Semántico sayk'uy · 52

renaser/kernel/src/async_system/teclado.rs

renaser/kernel/src/compositor.rs

renaser/kernel/src/consola.rs

renaser/kernel/src/grafico.rs

renaser/kernel/src/main.rs

renaser/kernel/src/wasm/env.rs

renaser/kernel/src/wasm/mod.rs

12 qillqakuna · +673 −174

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<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.
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`.
-
-Líneas abiertas posteriores: más capacidades del host (temporización, audio).
+- **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: 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<ArrayQueue<u8>>;
 
-/// Censo de canales — uno por aplicacion del userspace. El manejador de IRQ1
-/// difunde cada scancode a TODOS: asi cada app recibe su propia copia del
-/// evento, sin que una le arrebate la pulsacion a otra.
-static CANALES: Once<Mutex<Vec<CanalTeclado>>> = Once::new();
+/// 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<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
 /// 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()));
+        interrupts::without_interrupts(|| {
+            let mut censo = censo.lock();
+            while censo.len() <= indice {
+                censo.push(None);
+            }
+            censo[indice] = Some(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) {
+/// 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() {
         interrupts::without_interrupts(|| {
-            censo.lock().retain(|inscrito| !Arc::ptr_eq(inscrito, canal));
+            let mut censo = censo.lock();
+            if let Some(ranura) = censo.get_mut(indice) {
+                *ranura = None;
+            }
         });
     }
 }
 
-/// 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.
+/// 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() {
-        for canal in censo.lock().iter() {
-            // Si un canal desborda, se descarta el scancode en silencio: mas
+        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<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
 /// 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<RegionPantalla> {
-    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<RegionPantalla> {
+    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<Re
         area.alto as i32,
     );
     let params = LayoutParams {
-        mode: MODO,
+        mode: modo,
         gap: MARGEN,
         ..LayoutParams::default()
     };
@@ -69,9 +350,8 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<Re
         .collect()
 }
 
-/// Traduce un `Rect` de `mirada-layout` (`i32`, en teoria con signo) a la
-/// `RegionPantalla` del kernel (`usize`). Un rectangulo degenerado queda en
-/// cero — el kernel no compondra nada en el.
+/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
+/// kernel (`usize`). Un rectangulo degenerado queda en cero.
 fn rect_a_region(r: Rect) -> 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<Mutex<Consola>> = Once::new();
 
-/// Puerta del kernel para la capacidad `sys_render_frame` del userspace WASM:
-/// compone sobre la consola global un fotograma —cuyos limites el host ya
-/// verifico matematicamente contra la memoria lineal del modulo— 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<ContextoCapacidades>,
     /// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
     /// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
     /// el handle de la `Instance` no aporta nada y no se retiene.
     func_tick: TypedFunc<(), ()>,
-    /// 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);
-
-        Ok(AplicacionWasm {
-            almacen,
-            func_tick,
-            marco,
-        })
+        //    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 })
     }
 
     /// 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);
     }
 }