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` 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 + 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>> = 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 + 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 = { + 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 + 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>; @@ -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, + /// ¿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> = 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, + 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> = 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 { // 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 = 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()); } }