gioser

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definitions

feat(renaser): Fase 10 — alta y baja de aplicaciones en vivo

El censo de aplicaciones deja de fijarse en el arranque: una app puede
nacer o cerrarse con el reactor ya en marcha.

- El reactor admite NACIMIENTOS en vivo: cola `NACIMIENTOS` +
  `engendrar()`, drenada al inicio de cada vuelta de `run()`;
  `Task::adoptar` acoge un futuro ya empaquetado.
- `Alt+Q` (`Mando::Cerrar`): baja limpia. El compositor saca la
  ventana enfocada del teselado y del orden-Z; la app advierte la
  baja (`ventana_cerrada`) y concluye su tarea — su memoria, su
  combustible y su canal de teclado se liberan. Sin baliza.
- `Alt+N` (`Mando::Lanzar`): alta en vivo. `nacer_ventana` añade la
  ventana y entrega su índice; el orquestador instancia el WASM y
  engendra su tarea. Las apps de génesis dejan su bytecode cacheado
  como `Plantilla`; cada `Alt+N` instancia una en rotación.

Verificado en QEMU (sendkey): tres Alt+N hacen crecer el escritorio
de 5 a 8 ventanas; tres Alt+Q lo reducen de 8 a 5. Kernel estable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

089afccbbc9ef33670f49de864c3c77da027b2df · by sergio · (5 months ago) · parent 8f946b449 · browse the tree at this commit

Semantic delta · 32

renaser/kernel/src/async_system/executor.rs

renaser/kernel/src/async_system/task.rs

renaser/kernel/src/async_system/teclado.rs

renaser/kernel/src/compositor.rs

renaser/kernel/src/main.rs

9 files · +395 −16

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index a150fc9..44c1512 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -805,3 +805,45 @@ propio y libre, por encima de las demás.
   ventana la flota en cascada, solapando a la primera. `Alt+K` devuelve el foco
   a la ventana grande y ésta sube al frente, tapando por completo a la pequeña.
   Un `Alt+F` final la reintegra al teselado.
+
+## Fase 10 — Alta y baja de aplicaciones en vivo — 2026-05-22
+
+Hasta la Fase 9 el censo de aplicaciones se fijaba en el arranque: las apps
+nacían del manifiesto y sólo morían al fallar. La Fase 10 lo vuelve DINÁMICO —
+una app puede nacer o cerrarse con el reactor ya en marcha.
+
+### Añadido
+- **El reactor admite nacimientos en vivo.** `executor`: una cola de
+  NACIMIENTOS (`Vec<FuturoTarea>` tras un `Mutex`) y la función `engendrar`.
+  El ejecutor la drena al inicio de cada vuelta (`recoger_nacimientos`) y
+  adopta cada futuro como tarea. `Task::adoptar` acoge un futuro ya
+  empaquetado. `dormir_si_inactivo` cuenta los nacimientos como trabajo.
+- **Baja en vivo — `Alt+Q`.** Mando `Cerrar`: el compositor marca la ventana
+  enfocada como `cerrada`, libera su caché de respaldo, la saca del teselado y
+  del orden-Z, y traslada el foco a una ventana viva contigua. La app, en su
+  tarea, consulta `compositor::ventana_cerrada` cada fotograma; al verla
+  cerrada concluye su tarea — y `AplicacionWasm::drop` libera su memoria
+  lineal, su combustible y su canal de teclado. Una baja LIMPIA, sin baliza.
+- **Alta en vivo — `Alt+N`.** Mando `Lanzar`: el compositor cuenta la petición
+  (`PARTOS`); la tarea del compositor la atiende con `partos_pendientes` y
+  `lanzar_app`. `compositor::nacer_ventana` añade la ventana y devuelve su
+  índice; el orquestador instancia el WASM con ese índice y `engendra` su
+  tarea. Las apps de génesis dejan su bytecode cacheado en RAM como
+  `Plantilla`; cada `Alt+N` instancia la siguiente en rotación, sin volver al
+  disco —una E/S por sondeo en mitad del reactor sería un mal vecino—.
+- `compositor`: campo `Ventana.cerrada`, mandos `Cerrar` / `Lanzar`, y las
+  funciones `cerrar`, `nacer_ventana`, `ventana_cerrada`, `partos_pendientes`.
+- `teclado`: `Alt+Q` → `Cerrar`, `Alt+N` → `Lanzar`.
+
+### Cambiado
+- `encender_app` devuelve la `Plantilla` de la app —su bytecode y geometría—
+  para los lanzamientos en vivo.
+- `tarea_aplicacion` consulta `ventana_cerrada` antes de cada `tick`.
+- `presentar_fotograma` y `desalojar` ignoran una ventana ya cerrada: una baja
+  limpia gana a un fotograma o a un desalojo que lleguen tarde.
+
+### Verificado
+- QEMU (`sendkey`): tres `Alt+N` dan a luz tres apps nuevas y el escritorio se
+  re-tesela de 5 a 8 ventanas. Tres `Alt+Q` cierran la app enfocada una a una
+  y el teselado reclama su espacio, de 8 de vuelta a 5. El kernel sigue estable
+  a través de todas las altas y bajas.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index 728958e..2e67c1f 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -76,8 +76,9 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
 Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de
 objetos—, la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
 con `mirada-layout` (8a), ciclado de layout (8b), foco y enrutamiento selectivo
-del teclado (8c), promoción y reordenación de ventanas (8d)— y la Fase 9
-COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—.
+del teclado (8c), promoción y reordenación de ventanas (8d)—, la Fase 9
+COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—
+y la Fase 10 COMPLETA —alta y baja de aplicaciones en vivo (`Alt+N` / `Alt+Q`)—.
 Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index abe4211..c0558d3 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -414,6 +414,26 @@ Y hubo una regla pequeña y elegante: el cuarto flotante en que se posa la
 mirada sube siempre al frente. Mirar algo, en esta casa, es traerlo a primer
 plano.
 
+## La casa que respira — inquilinos que llegan y se van
+
+Una casa de verdad no tiene un número fijo de habitantes para siempre. Llegan
+nuevos, se marchan otros; la casa se acomoda. Hasta hoy, la de renaser era de
+censo cerrado: sus inquilinos entraban todos a la vez, al amanecer, y sólo se
+iban si tropezaban. No se podía invitar a nadie más, ni despedir a nadie en paz.
+
+Hoy la casa aprendió a respirar. Con una tecla se invita a un inquilino nuevo:
+aparece su cuarto, los demás se corren para hacerle sitio y se instala con sus
+cosas. Con otra tecla se despide al inquilino del cuarto en que está puesta la
+mirada: recoge en silencio, su cuarto se desvanece y el espacio que deja lo
+reparten los que quedan. Una despedida serena —no un tropiezo, no una alarma—:
+simplemente, ya no está.
+
+Para que un inquilino pudiera llegar tarde, el ama de llaves —el reactor— tuvo
+que cambiar una costumbre. Antes apuntaba a todos en su libro al abrir la casa
+y no volvía a tocar la lista. Ahora tiene una bandeja donde van dejándose los
+recién llegados, y en cada ronda la mira y les da su sitio. La casa ya no se
+escribe entera de una vez: se va escribiendo, día a día, mientras se vive.
+
 ---
 
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index 745e85e..ff74c7e 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -163,8 +163,26 @@ el teselado y FLOTAR sobre las demás. Verificada en QEMU (`sendkey`).
 - El foco recorre todas las ventanas; al posarse en una flotante, la alza al
   frente: la flotante enfocada está siempre delante.
 
-Líneas abiertas posteriores: alta y baja de aplicaciones en vivo; más
-capacidades del host (temporización, audio).
+## Fase 10 — alta y baja de aplicaciones en vivo (completada)
+
+Hasta la Fase 9 el censo de aplicaciones se fijaba en el arranque. La Fase 10
+lo vuelve DINÁMICO: una app puede nacer o cerrarse con el reactor ya en marcha.
+Verificada en QEMU (`sendkey`).
+
+- El reactor admite NACIMIENTOS en vivo: una cola que `engendrar` alimenta y
+  que el ejecutor drena al inicio de cada vuelta, adoptando cada futuro como
+  tarea. El censo de tareas deja de ser inmutable tras el arranque.
+- `Alt+Q` cierra la app enfocada: una baja LIMPIA. El compositor saca la
+  ventana del teselado y del orden-Z; la app, al advertir la baja, concluye su
+  tarea y `AplicacionWasm::drop` libera su memoria, su combustible y su canal.
+- `Alt+N` lanza una app nueva: `nacer_ventana` la añade y entrega su índice; el
+  orquestador instancia el WASM y engendra su tarea. Las apps de génesis dejan
+  su bytecode cacheado como plantilla; cada `Alt+N` instancia una en rotación.
+- El censo de ventanas sólo crece —los índices son la identidad, jamás se
+  reciclan—; una ventana cerrada queda como ranura inerte, fuera del teselado.
+
+Líneas abiertas posteriores: más capacidades del host (temporización, audio);
+reciclado de las ranuras de ventana cerradas.
 
 ## Principios que persisten entre fases
 
diff --git a/renaser/kernel/src/async_system/executor.rs b/renaser/kernel/src/async_system/executor.rs
index f6c3dbb..d47ab22 100644
--- a/renaser/kernel/src/async_system/executor.rs
+++ b/renaser/kernel/src/async_system/executor.rs
@@ -5,19 +5,46 @@
 //  vivas y una cola de las que estan listas para avanzar. Cuando no queda nada
 //  por hacer, no malgasta la CPU en un bucle ocupado: la duerme con `hlt`
 //  hasta que el proximo impulso de hardware la despierte.
+//
+//  FASE 10 :: el censo deja de ser inmutable tras el arranque. Una cola de
+//  NACIMIENTOS permite engendrar tareas EN VIVO —con el reactor ya en marcha—:
+//  el orquestador deposita un futuro y el ejecutor lo adopta en su proxima
+//  vuelta. Asi el userspace puede crecer despues del arranque.
 // =============================================================================
 
+use alloc::boxed::Box;
 use alloc::collections::{BTreeMap, VecDeque};
 use alloc::sync::Arc;
+use alloc::vec::Vec;
 use core::future::Future;
+use core::pin::Pin;
 use core::task::{Context, Poll, Waker};
 
-use spin::Mutex;
+use spin::{Mutex, Once};
 use x86_64::instructions::interrupts;
 
 use super::task::{Task, TaskId};
 use super::waker::{self, ColaListas};
 
+/// Un futuro de tarea ya anclado en el heap: la moneda de los nacimientos.
+pub type FuturoTarea = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
+
+/// Cola de NACIMIENTOS: tareas engendradas EN VIVO, mientras el reactor ya
+/// corre. Una tarea cooperativa deposita aqui un futuro; el ejecutor lo recoge
+/// al inicio de su proxima vuelta y lo da de alta. No la toca ningun manejador
+/// de IRQ —solo tareas cooperativas y el propio ejecutor—, asi que un `Mutex`
+/// llano basta: en un solo nucleo cooperativo nadie disputa el cerrojo.
+static NACIMIENTOS: Once<Mutex<Vec<FuturoTarea>>> = Once::new();
+
+/// Engendra una tarea nueva mientras el reactor ya corre (Fase 10). El ejecutor
+/// la adoptara en su proxima vuelta. La invocan los orquestadores del kernel
+/// —jamas una IRQ—.
+pub fn engendrar(futuro: FuturoTarea) {
+    if let Some(nacimientos) = NACIMIENTOS.get() {
+        nacimientos.lock().push(futuro);
+    }
+}
+
 /// El ejecutor cooperativo de renaser.
 pub struct Executor {
     /// Censo de todas las tareas vivas, indexadas por su identidad.
@@ -31,6 +58,7 @@ pub struct Executor {
 impl Executor {
     /// Crea un ejecutor vacio. Requiere que el heap ya este fundado.
     pub fn nuevo() -> Executor {
+        NACIMIENTOS.call_once(|| Mutex::new(Vec::new()));
         Executor {
             tareas: BTreeMap::new(),
             cola_listas: Arc::new(Mutex::new(VecDeque::new())),
@@ -40,7 +68,12 @@ impl Executor {
 
     /// Da de alta una tarea nueva y la marca como lista para su primer avance.
     pub fn spawn(&mut self, futuro: impl Future<Output = ()> + Send + 'static) {
-        let tarea = Task::nueva(futuro);
+        self.alistar(Task::nueva(futuro));
+    }
+
+    /// Inscribe una tarea ya construida en el censo y la encola para su primer
+    /// avance. Es la via comun de `spawn` y de la recoleccion de nacimientos.
+    fn alistar(&mut self, tarea: Task) {
         let id = tarea.id;
         if self.tareas.insert(id, tarea).is_some() {
             panic!("renaser :: TaskId duplicado — lo imposible ha ocurrido");
@@ -48,6 +81,24 @@ impl Executor {
         interrupts::without_interrupts(|| self.cola_listas.lock().push_back(id));
     }
 
+    /// Recoge las tareas engendradas EN VIVO desde la ultima vuelta y las da de
+    /// alta. Se invoca al inicio de cada ciclo del reactor (Fase 10).
+    fn recoger_nacimientos(&mut self) {
+        let Some(nacimientos) = NACIMIENTOS.get() else {
+            return;
+        };
+        let recien: Vec<FuturoTarea> = {
+            let mut cola = nacimientos.lock();
+            if cola.is_empty() {
+                return;
+            }
+            core::mem::take(&mut *cola)
+        };
+        for futuro in recien {
+            self.alistar(Task::adoptar(futuro));
+        }
+    }
+
     /// Avanza, hasta agotarla, la cola de tareas listas.
     fn avanzar_listas(&mut self) {
         loop {
@@ -79,11 +130,17 @@ impl Executor {
 
     /// Si no queda trabajo, duerme la CPU hasta la proxima interrupcion. El
     /// chequeo y el `hlt` se hacen con las interrupciones acalladas para que
-    /// ningun despertar se pierda en la rendija entre uno y otro.
+    /// ningun despertar se pierda en la rendija entre uno y otro. Una tarea
+    /// recien engendrada cuenta como trabajo: no se duerme con nacimientos
+    /// pendientes de adoptar.
     fn dormir_si_inactivo(&self) {
         interrupts::disable();
-        let hay_trabajo = !self.cola_listas.lock().is_empty();
-        if hay_trabajo {
+        let hay_listas = !self.cola_listas.lock().is_empty();
+        let hay_nacimientos = NACIMIENTOS
+            .get()
+            .map(|nacimientos| !nacimientos.lock().is_empty())
+            .unwrap_or(false);
+        if hay_listas || hay_nacimientos {
             // Llego trabajo justo ahora: reactivar y seguir sin dormir.
             interrupts::enable();
         } else {
@@ -96,6 +153,7 @@ impl Executor {
     /// vive del latir de sus interrupciones.
     pub fn run(&mut self) -> ! {
         loop {
+            self.recoger_nacimientos();
             self.avanzar_listas();
             self.dormir_si_inactivo();
         }
diff --git a/renaser/kernel/src/async_system/task.rs b/renaser/kernel/src/async_system/task.rs
index 5a4b008..e56a999 100644
--- a/renaser/kernel/src/async_system/task.rs
+++ b/renaser/kernel/src/async_system/task.rs
@@ -38,6 +38,16 @@ impl Task {
         }
     }
 
+    /// Adopta un `Future` ya anclado en el heap como una tarea con identidad
+    /// propia. Es la via de los nacimientos en vivo (Fase 10): el orquestador
+    /// entrega el futuro ya empaquetado y el ejecutor lo acoge sin tocarlo.
+    pub fn adoptar(futuro: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> Task {
+        Task {
+            id: TaskId::nuevo(),
+            futuro,
+        }
+    }
+
     /// Hace avanzar la tarea un paso. `Poll::Ready` significa que concluyo.
     pub fn poll(&mut self, contexto: &mut Context) -> Poll<()> {
         self.futuro.as_mut().poll(contexto)
diff --git a/renaser/kernel/src/async_system/teclado.rs b/renaser/kernel/src/async_system/teclado.rs
index b28239c..5ac2b1b 100644
--- a/renaser/kernel/src/async_system/teclado.rs
+++ b/renaser/kernel/src/async_system/teclado.rs
@@ -12,8 +12,8 @@
 //
 //    * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make
 //      codes son MANDOS del compositor (ciclar el teselado, mover el foco,
-//      promover, reordenar y hacer flotar ventanas): se consumen aqui, jamas
-//      llegan a una app.
+//      promover, reordenar y hacer flotar ventanas, cerrar y lanzar apps): 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.
@@ -54,6 +54,10 @@ const TECLA_L: u8 = 0x26;
 const ENTER: u8 = 0x1C;
 /// Tecla F — `Alt + F` alterna la ventana enfocada entre teselada y flotante.
 const TECLA_F: u8 = 0x21;
+/// Tecla Q — `Alt + Q` cierra la aplicacion enfocada (baja en vivo).
+const TECLA_Q: u8 = 0x10;
+/// Tecla N — `Alt + N` lanza una aplicacion nueva (alta en vivo).
+const TECLA_N: u8 = 0x31;
 
 /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
 pub type CanalTeclado = Arc<ArrayQueue<u8>>;
@@ -140,6 +144,8 @@ pub fn recibir_scancode(scancode: u8) {
             TECLA_L => compositor::solicitar(Mando::MoverAdelante),
             TECLA_H => compositor::solicitar(Mando::MoverAtras),
             TECLA_F => compositor::solicitar(Mando::Flotar),
+            TECLA_Q => compositor::solicitar(Mando::Cerrar),
+            TECLA_N => compositor::solicitar(Mando::Lanzar),
             _ => {}
         }
         return;
diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs
index 9ee5ce6..5b3d401 100644
--- a/renaser/kernel/src/compositor.rs
+++ b/renaser/kernel/src/compositor.rs
@@ -21,6 +21,14 @@
 //  RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
 //  resuelva por el orden del pintado, sin recortes ni mascaras.
 //
+//  FASE 10 :: el escritorio deja de ser un censo fijo. Una ventana puede
+//  CERRARSE en vivo (`Alt+Q`): se la marca, su app concluye su tarea por su
+//  voluntad y el teselado reclama su espacio. Y puede NACER una ventana nueva
+//  (`Alt+N`): `nacer_ventana` la añade al censo y devuelve su indice al
+//  orquestador, que instancia su WASM y engendra su tarea. El censo de
+//  ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
+//  ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
+//
 //  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
@@ -82,6 +90,10 @@ pub enum Mando {
     MoverAtras,
     /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
     Flotar,
+    /// Cerrar la aplicacion enfocada — una baja limpia, en vivo (Fase 10).
+    Cerrar,
+    /// Lanzar una aplicacion nueva — un alta en vivo (Fase 10).
+    Lanzar,
 }
 
 /// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
@@ -103,6 +115,10 @@ struct Ventana {
     /// 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>,
+    /// ¿Se ha pedido cerrar esta ventana en vivo (Fase 10)? Una vez `true`, su
+    /// app concluye su tarea, la ranura queda inerte —fuera del orden, del
+    /// orden-Z y del foco— y el teselado reclama su espacio.
+    cerrada: bool,
 }
 
 /// El escritorio: el registro de todas las ventanas y el modo de teselado.
@@ -138,6 +154,12 @@ static FOCO: AtomicUsize = AtomicUsize::new(0);
 /// compositor las drena desde el reactor cooperativo.
 static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
 
+/// Cuantos lanzamientos de aplicacion (Fase 10) aguardan. Lo incrementa
+/// `atender_mandos` al recibir un `Mando::Lanzar`; lo drena `partos_pendientes`,
+/// que lo lee el orquestador del kernel —el unico que sabe instanciar un WASM—.
+/// Atomico: el compositor lo escribe, el orquestador lo lee y lo pone a cero.
+static PARTOS: AtomicUsize = AtomicUsize::new(0);
+
 // =============================================================================
 //  Fundacion y consulta — el arranque
 // =============================================================================
@@ -164,6 +186,7 @@ pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
             cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
             pintada: false,
             baliza: None,
+            cerrada: false,
         });
     }
 
@@ -246,6 +269,11 @@ pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
         let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
             return;
         };
+        // Una ventana cerrada (Fase 10) ya no se pinta: su app pudo emitir un
+        // ultimo fotograma antes de que su tarea advirtiera la baja.
+        if ventana.cerrada {
+            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());
@@ -280,6 +308,11 @@ pub fn desalojar(indice: usize, color: Color) {
         let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
             return;
         };
+        // Una ventana ya cerrada (Fase 10) no recibe baliza: la baja limpia
+        // gana a un desalojo que llegue tarde, en la misma vuelta.
+        if ventana.cerrada {
+            return;
+        }
         ventana.baliza = Some(color);
     }
 
@@ -313,6 +346,13 @@ pub fn atender_mandos() {
             Mando::MoverAdelante => mover_ventana(true),
             Mando::MoverAtras => mover_ventana(false),
             Mando::Flotar => flotar(),
+            Mando::Cerrar => cerrar(),
+            // El alta de una app necesita instanciar un WASM — algo que el
+            // compositor no sabe hacer—. Solo se cuenta la peticion; el
+            // orquestador del kernel la atendera (ver `partos_pendientes`).
+            Mando::Lanzar => {
+                PARTOS.fetch_add(1, Ordering::Relaxed);
+            }
         }
     }
 }
@@ -516,6 +556,106 @@ fn recomponer(escritorio: &Escritorio) {
     consola::recomponer(area, &capas);
 }
 
+// =============================================================================
+//  FASE 10 — alta y baja de aplicaciones en vivo
+// =============================================================================
+
+/// Cierra la aplicacion enfocada (`Alt+Q`): una baja LIMPIA, distinta del
+/// desalojo por falla. Marca la ventana como cerrada, libera su cache de
+/// respaldo, la saca del teselado y del orden-Z, y traslada el foco a una
+/// ventana viva contigua. La app, en su tarea, advertira la baja y concluira.
+fn cerrar() {
+    let Some(escritorio) = ESCRITORIO.get() else {
+        return;
+    };
+    let mut escritorio = escritorio.lock();
+    let foco = FOCO.load(Ordering::Relaxed);
+    // Solo se cierra una ventana viva. El foco jamas se posa en una muerta o
+    // cerrada, pero la guarda lo deja explicito.
+    match escritorio.ventanas.get(foco) {
+        Some(v) if v.baliza.is_none() && !v.cerrada => {}
+        _ => return,
+    }
+    // Marcar la baja y liberar el respaldo: la cache de un fotograma puede
+    // pesar un megabyte — no tiene sentido retenerla en una ranura inerte.
+    let ventana = &mut escritorio.ventanas[foco];
+    ventana.cerrada = true;
+    ventana.pintada = false;
+    ventana.cache = Vec::new();
+    // Sacarla del teselado y del orden-Z. El censo conserva la ranura —los
+    // indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
+    escritorio.orden.retain(|&v| v != foco);
+    escritorio.flotantes.retain(|&v| v != foco);
+    // El foco salta a la primera ventana viva que quede; si no queda ninguna,
+    // se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
+    let nuevo = escritorio
+        .orden
+        .iter()
+        .chain(escritorio.flotantes.iter())
+        .copied()
+        .find(|&v| {
+            let w = &escritorio.ventanas[v];
+            w.baliza.is_none() && !w.cerrada
+        })
+        .unwrap_or(foco);
+    FOCO.store(nuevo, Ordering::Relaxed);
+    alzar_si_flota(&mut escritorio, nuevo);
+    aplicar_teselado(&mut escritorio);
+    recomponer(&escritorio);
+}
+
+/// Da de alta una ventana NUEVA y devuelve su indice —su identidad—. La crea
+/// con su cache de respaldo al tamaño natural, la añade al final del orden de
+/// teselado, recalcula el teselado y recompone. La invoca el orquestador del
+/// kernel justo antes de instanciar el WASM de la app, que necesita ese indice.
+pub fn nacer_ventana(nat_ancho: usize, nat_alto: usize) -> usize {
+    let Some(escritorio) = ESCRITORIO.get() else {
+        return 0;
+    };
+    let mut escritorio = escritorio.lock();
+    let indice = escritorio.ventanas.len();
+    escritorio.ventanas.push(Ventana {
+        natural_ancho: nat_ancho,
+        natural_alto: nat_alto,
+        marco: RegionPantalla {
+            x: 0,
+            y: 0,
+            ancho: 0,
+            alto: 0,
+        },
+        cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
+        pintada: false,
+        baliza: None,
+        cerrada: false,
+    });
+    escritorio.orden.push(indice);
+    aplicar_teselado(&mut escritorio);
+    recomponer(&escritorio);
+    indice
+}
+
+/// ¿Se ha pedido cerrar la ventana `indice`? Cada app la consulta en su tarea,
+/// fotograma a fotograma: cuando es `true`, concluye su tarea y se libera. Una
+/// ventana inexistente cuenta como cerrada.
+pub fn ventana_cerrada(indice: usize) -> bool {
+    let Some(escritorio) = ESCRITORIO.get() else {
+        return false;
+    };
+    escritorio
+        .lock()
+        .ventanas
+        .get(indice)
+        .map(|ventana| ventana.cerrada)
+        .unwrap_or(true)
+}
+
+/// Cuantas aplicaciones nuevas se han pedido lanzar desde la ultima consulta —y
+/// pone el contador a cero—. La invoca el orquestador del kernel —el unico que
+/// sabe instanciar un WASM— en cada fotograma de la tarea del compositor.
+pub fn partos_pendientes() -> usize {
+    PARTOS.swap(0, Ordering::Relaxed)
+}
+
 // =============================================================================
 //  Teselado — la geometria pura de `mirada-layout`
 // =============================================================================
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index 2a96c6a..90c0336 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -36,12 +36,13 @@
 
 extern crate alloc; // El heap esta vivo: `alloc::*` queda disponible (Fase 3).
 
+use alloc::boxed::Box;
 use alloc::format;
 
 use bootloader_api::config::{BootloaderConfig, Mapping};
 use bootloader_api::info::{FrameBufferInfo, MemoryRegionKind, MemoryRegions, PixelFormat};
 use bootloader_api::{entry_point, BootInfo};
-use spin::Mutex;
+use spin::{Mutex, Once};
 
 // --- Subsistemas del kernel ---
 mod almacen;
@@ -64,6 +65,7 @@ mod wasm;
 pub(crate) use sync::CeldaSync;
 
 use alloc::vec::Vec;
+use core::sync::atomic::{AtomicUsize, Ordering};
 
 use async_system::executor::Executor;
 use baliza::BALIZA_PANICO;
@@ -91,6 +93,23 @@ pub(crate) fn detener() -> ! {
     }
 }
 
+/// FASE 10 :: el molde de una aplicacion para los lanzamientos EN VIVO. Guarda
+/// su bytecode —cacheado en RAM al arrancar, para no volver al disco despues—
+/// y la geometria y la cuota de memoria con que instanciarla.
+struct Plantilla {
+    bytecode: Vec<u8>,
+    nat_ancho: usize,
+    nat_alto: usize,
+    techo: usize,
+}
+
+/// Las plantillas de las apps de genesis. Se fijan una vez, en el arranque;
+/// cada `Alt+N` instancia la siguiente en rotacion.
+static PLANTILLAS: Once<Vec<Plantilla>> = Once::new();
+
+/// El cursor rotatorio sobre `PLANTILLAS`: que app nace en el proximo `Alt+N`.
+static CURSOR_PLANTILLA: AtomicUsize = AtomicUsize::new(0);
+
 /// 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
@@ -99,6 +118,13 @@ pub(crate) fn detener() -> ! {
 async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
     loop {
         async_system::reloj::EsperaFrame::nueva().await;
+        // ¿El compositor pidio cerrar esta ventana (`Alt+Q`)? La tarea concluye
+        // por su propia voluntad: al retornar, `AplicacionWasm` se libera —su
+        // memoria lineal, su combustible, su canal de teclado— y el ejecutor la
+        // retira del censo. Una baja LIMPIA, sin baliza (Fase 10).
+        if compositor::ventana_cerrada(app.indice()) {
+            return;
+        }
         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. El compositor
@@ -117,6 +143,12 @@ async fn tarea_compositor() {
     loop {
         async_system::reloj::EsperaFrame::nueva().await;
         compositor::atender_mandos();
+        // FASE 10 :: atender las altas en vivo. Por cada `Alt+N` pendiente,
+        // dar a luz una aplicacion nueva — el compositor solo conto la
+        // peticion; instanciar el WASM es trabajo del orquestador.
+        for _ in 0..compositor::partos_pendientes() {
+            lanzar_app();
+        }
     }
 }
 
@@ -148,7 +180,11 @@ async fn tarea_sonda_disco() {
 /// 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) {
+fn encender_app(
+    ejecutor: &mut Executor,
+    indice: usize,
+    entrada: &manifiesto::EntradaApp,
+) -> Option<Plantilla> {
     // El tamaño NATURAL del lienzo de la app —lo que sabe pintar, fijo— lo
     // dicta su `EntradaApp`; el compositor decide en que marco lo coloca.
     let natural = manifiesto::region(entrada);
@@ -159,7 +195,7 @@ fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::En
         Ok(Some(objeto)) => objeto.datos,
         _ => {
             compositor::desalojar(indice, Color::DESALOJO);
-            return;
+            return None;
         }
     };
     // `indice` es la identidad de la app: su ventana en el escritorio del
@@ -174,6 +210,48 @@ fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::En
         Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
         Err(_) => compositor::desalojar(indice, Color::DESALOJO),
     }
+    // FASE 10 :: el bytecode, ya recuperado y verificado, queda como PLANTILLA:
+    // un molde en RAM con que `Alt+N` instanciara copias en vivo, sin volver al
+    // disco —que la E/S por sondeo en mitad del reactor seria un mal vecino—.
+    Some(Plantilla {
+        bytecode,
+        nat_ancho: natural.ancho,
+        nat_alto: natural.alto,
+        techo: entrada.techo_memoria as usize,
+    })
+}
+
+/// FASE 10 :: da a luz una aplicacion EN VIVO. Elige la siguiente plantilla en
+/// rotacion, abre su ventana en el compositor —que le asigna su indice—,
+/// instancia su WASM con ese indice y engendra su tarea en el reactor ya en
+/// marcha. Si la carga falla, la ventana recien nacida se desaloja; el kernel
+/// sigue. La invoca la tarea del compositor al atender un `Alt+N`.
+fn lanzar_app() {
+    let Some(plantillas) = PLANTILLAS.get() else {
+        return;
+    };
+    if plantillas.is_empty() {
+        return;
+    }
+    // El cursor rota sobre las plantillas: cada `Alt+N` engendra la siguiente.
+    let cursor = CURSOR_PLANTILLA.fetch_add(1, Ordering::Relaxed);
+    let plantilla = &plantillas[cursor % plantillas.len()];
+
+    // La ventana nace primero: el compositor le entrega su indice —su
+    // identidad—, que el WASM necesita para hallar su ventana y su canal.
+    let indice = compositor::nacer_ventana(plantilla.nat_ancho, plantilla.nat_alto);
+    match wasm::AplicacionWasm::cargar(
+        &plantilla.bytecode,
+        plantilla.nat_ancho,
+        plantilla.nat_alto,
+        plantilla.techo,
+        indice,
+    ) {
+        // La tarea se ENGENDRA, no se hace `spawn`: el reactor ya corre y el
+        // ejecutor la adoptara en su proxima vuelta (Fase 10).
+        Ok(app) => async_system::executor::engendrar(Box::pin(tarea_aplicacion(app))),
+        Err(_) => compositor::desalojar(indice, Color::DESALOJO),
+    }
 }
 
 /// Escribe una linea en la consola global y la presenta. Atajo para los
@@ -235,12 +313,18 @@ fn cargar_userspace(ejecutor: &mut Executor, ancho_pantalla: usize, alto_pantall
         compositor::fundar(ancho_pantalla, alto_pantalla, &naturales);
         compositor::componer_escenario();
 
+        let mut plantillas: Vec<Plantilla> = Vec::new();
         for (indice, entrada) in m.apps.iter().enumerate() {
-            encender_app(ejecutor, indice, entrada);
+            if let Some(plantilla) = encender_app(ejecutor, indice, entrada) {
+                plantillas.push(plantilla);
+            }
         }
+        // FASE 10 :: fijar las plantillas de las apps. A partir de aqui, cada
+        // `Alt+N` instancia una copia viva, en rotacion.
+        PLANTILLAS.call_once(|| plantillas);
 
         // La tarea del compositor: atiende los mandos del teclado —ciclar el
-        // teselado, mover el foco— en cada fotograma del reactor.
+        // teselado, mover el foco, cerrar y lanzar apps— en cada fotograma.
         ejecutor.spawn(tarea_compositor());
     }
 }