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>
This commit is contained in:
sergio
2026-05-22 20:26:25 +00:00
parent 8f946b449c
commit 089afccbbc
9 changed files with 395 additions and 16 deletions
+42
View File
@@ -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 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. a la ventana grande y ésta sube al frente, tapando por completo a la pequeña.
Un `Alt+F` final la reintegra al teselado. 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.
+3 -2
View File
@@ -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 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 objetos—, la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
con `mirada-layout` (8a), ciclado de layout (8b), foco y enrutamiento selectivo 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 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`)—. 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`. Todo verificado en QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo ## Flujo de trabajo
+20
View File
@@ -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 mirada sube siempre al frente. Mirar algo, en esta casa, es traerlo a primer
plano. 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.* *El diario continúa. La próxima página la escribirá la próxima jornada.*
+20 -2
View File
@@ -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 - El foco recorre todas las ventanas; al posarse en una flotante, la alza al
frente: la flotante enfocada está siempre delante. frente: la flotante enfocada está siempre delante.
Líneas abiertas posteriores: alta y baja de aplicaciones en vivo; más ## Fase 10 — alta y baja de aplicaciones en vivo (completada)
capacidades del host (temporización, audio).
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 ## Principios que persisten entre fases
+63 -5
View File
@@ -5,19 +5,46 @@
// vivas y una cola de las que estan listas para avanzar. Cuando no queda nada // 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` // por hacer, no malgasta la CPU en un bucle ocupado: la duerme con `hlt`
// hasta que el proximo impulso de hardware la despierte. // 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::collections::{BTreeMap, VecDeque};
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::vec::Vec;
use core::future::Future; use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll, Waker}; use core::task::{Context, Poll, Waker};
use spin::Mutex; use spin::{Mutex, Once};
use x86_64::instructions::interrupts; use x86_64::instructions::interrupts;
use super::task::{Task, TaskId}; use super::task::{Task, TaskId};
use super::waker::{self, ColaListas}; 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. /// El ejecutor cooperativo de renaser.
pub struct Executor { pub struct Executor {
/// Censo de todas las tareas vivas, indexadas por su identidad. /// Censo de todas las tareas vivas, indexadas por su identidad.
@@ -31,6 +58,7 @@ pub struct Executor {
impl Executor { impl Executor {
/// Crea un ejecutor vacio. Requiere que el heap ya este fundado. /// Crea un ejecutor vacio. Requiere que el heap ya este fundado.
pub fn nuevo() -> Executor { pub fn nuevo() -> Executor {
NACIMIENTOS.call_once(|| Mutex::new(Vec::new()));
Executor { Executor {
tareas: BTreeMap::new(), tareas: BTreeMap::new(),
cola_listas: Arc::new(Mutex::new(VecDeque::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. /// 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) { 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; let id = tarea.id;
if self.tareas.insert(id, tarea).is_some() { if self.tareas.insert(id, tarea).is_some() {
panic!("renaser :: TaskId duplicado — lo imposible ha ocurrido"); panic!("renaser :: TaskId duplicado — lo imposible ha ocurrido");
@@ -48,6 +81,24 @@ impl Executor {
interrupts::without_interrupts(|| self.cola_listas.lock().push_back(id)); 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. /// Avanza, hasta agotarla, la cola de tareas listas.
fn avanzar_listas(&mut self) { fn avanzar_listas(&mut self) {
loop { loop {
@@ -79,11 +130,17 @@ impl Executor {
/// Si no queda trabajo, duerme la CPU hasta la proxima interrupcion. El /// Si no queda trabajo, duerme la CPU hasta la proxima interrupcion. El
/// chequeo y el `hlt` se hacen con las interrupciones acalladas para que /// 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) { fn dormir_si_inactivo(&self) {
interrupts::disable(); interrupts::disable();
let hay_trabajo = !self.cola_listas.lock().is_empty(); let hay_listas = !self.cola_listas.lock().is_empty();
if hay_trabajo { 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. // Llego trabajo justo ahora: reactivar y seguir sin dormir.
interrupts::enable(); interrupts::enable();
} else { } else {
@@ -96,6 +153,7 @@ impl Executor {
/// vive del latir de sus interrupciones. /// vive del latir de sus interrupciones.
pub fn run(&mut self) -> ! { pub fn run(&mut self) -> ! {
loop { loop {
self.recoger_nacimientos();
self.avanzar_listas(); self.avanzar_listas();
self.dormir_si_inactivo(); self.dormir_si_inactivo();
} }
+10
View File
@@ -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. /// Hace avanzar la tarea un paso. `Poll::Ready` significa que concluyo.
pub fn poll(&mut self, contexto: &mut Context) -> Poll<()> { pub fn poll(&mut self, contexto: &mut Context) -> Poll<()> {
self.futuro.as_mut().poll(contexto) self.futuro.as_mut().poll(contexto)
+8 -2
View File
@@ -12,8 +12,8 @@
// //
// * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make // * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make
// codes son MANDOS del compositor (ciclar el teselado, mover el foco, // codes son MANDOS del compositor (ciclar el teselado, mover el foco,
// promover, reordenar y hacer flotar ventanas): se consumen aqui, jamas // promover, reordenar y hacer flotar ventanas, cerrar y lanzar apps): se
// llegan a una app. // consumen aqui, jamas llegan a una app.
// * Una tecla ordinaria se entrega SOLO a la app ENFOCADA — la que el // * 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`, // compositor senala. El censo de canales se indexa por el `indice_app`,
// de modo que el foco —un atomico— elija el canal exacto. // de modo que el foco —un atomico— elija el canal exacto.
@@ -54,6 +54,10 @@ const TECLA_L: u8 = 0x26;
const ENTER: u8 = 0x1C; const ENTER: u8 = 0x1C;
/// Tecla F — `Alt + F` alterna la ventana enfocada entre teselada y flotante. /// Tecla F — `Alt + F` alterna la ventana enfocada entre teselada y flotante.
const TECLA_F: u8 = 0x21; 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. /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
pub type CanalTeclado = Arc<ArrayQueue<u8>>; pub type CanalTeclado = Arc<ArrayQueue<u8>>;
@@ -140,6 +144,8 @@ pub fn recibir_scancode(scancode: u8) {
TECLA_L => compositor::solicitar(Mando::MoverAdelante), TECLA_L => compositor::solicitar(Mando::MoverAdelante),
TECLA_H => compositor::solicitar(Mando::MoverAtras), TECLA_H => compositor::solicitar(Mando::MoverAtras),
TECLA_F => compositor::solicitar(Mando::Flotar), TECLA_F => compositor::solicitar(Mando::Flotar),
TECLA_Q => compositor::solicitar(Mando::Cerrar),
TECLA_N => compositor::solicitar(Mando::Lanzar),
_ => {} _ => {}
} }
return; return;
+140
View File
@@ -21,6 +21,14 @@
// RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se // RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
// resuelva por el orden del pintado, sin recortes ni mascaras. // 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 // EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
// cooperativas (el `tick` de una app, la tarea del compositor): el manejador // 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 // de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
@@ -82,6 +90,10 @@ pub enum Mando {
MoverAtras, MoverAtras,
/// Alternar la ventana enfocada entre teselada y flotante (Fase 9). /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
Flotar, 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. /// 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 /// 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. /// vive; `Some(color)` la marca como muerta y la excluye del foco.
baliza: Option<Color>, 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. /// 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. /// compositor las drena desde el reactor cooperativo.
static MANDOS: Once<ArrayQueue<Mando>> = Once::new(); 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 // 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)], cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
pintada: false, pintada: false,
baliza: None, 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 { let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return; 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 // Cachear el fotograma. El destino esta acotado al lienzo natural; se
// copia el minimo de ambas longitudes — jamas se desborda la cache. // copia el minimo de ambas longitudes — jamas se desborda la cache.
let n = ventana.cache.len().min(datos.len()); 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 { let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return; 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); ventana.baliza = Some(color);
} }
@@ -313,6 +346,13 @@ pub fn atender_mandos() {
Mando::MoverAdelante => mover_ventana(true), Mando::MoverAdelante => mover_ventana(true),
Mando::MoverAtras => mover_ventana(false), Mando::MoverAtras => mover_ventana(false),
Mando::Flotar => flotar(), 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); 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` // Teselado — la geometria pura de `mirada-layout`
// ============================================================================= // =============================================================================
+89 -5
View File
@@ -36,12 +36,13 @@
extern crate alloc; // El heap esta vivo: `alloc::*` queda disponible (Fase 3). extern crate alloc; // El heap esta vivo: `alloc::*` queda disponible (Fase 3).
use alloc::boxed::Box;
use alloc::format; use alloc::format;
use bootloader_api::config::{BootloaderConfig, Mapping}; use bootloader_api::config::{BootloaderConfig, Mapping};
use bootloader_api::info::{FrameBufferInfo, MemoryRegionKind, MemoryRegions, PixelFormat}; use bootloader_api::info::{FrameBufferInfo, MemoryRegionKind, MemoryRegions, PixelFormat};
use bootloader_api::{entry_point, BootInfo}; use bootloader_api::{entry_point, BootInfo};
use spin::Mutex; use spin::{Mutex, Once};
// --- Subsistemas del kernel --- // --- Subsistemas del kernel ---
mod almacen; mod almacen;
@@ -64,6 +65,7 @@ mod wasm;
pub(crate) use sync::CeldaSync; pub(crate) use sync::CeldaSync;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::sync::atomic::{AtomicUsize, Ordering};
use async_system::executor::Executor; use async_system::executor::Executor;
use baliza::BALIZA_PANICO; 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 /// Tarea cooperativa de una aplicacion WASM. En cada pulso del reloj le concede
/// un `tick` —un fotograma de trabajo— y cede la CPU hasta el siguiente; entre /// un `tick` —un fotograma de trabajo— y cede la CPU hasta el siguiente; entre
/// medias corren sus vecinas. Si la app falla o agota su combustible, se la /// medias corren sus vecinas. Si la app falla o agota su combustible, se la
@@ -99,6 +118,13 @@ pub(crate) fn detener() -> ! {
async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) { async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
loop { loop {
async_system::reloj::EsperaFrame::nueva().await; async_system::reloj::EsperaFrame::nueva().await;
// ¿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() { if let Err(falla) = app.tick() {
// El color de la baliza delata la causa: purpura si agoto su tiempo // El color de la baliza delata la causa: purpura si agoto su tiempo
// o aborto, amarillo si reviento su techo de memoria. El compositor // o aborto, amarillo si reviento su techo de memoria. El compositor
@@ -117,6 +143,12 @@ async fn tarea_compositor() {
loop { loop {
async_system::reloj::EsperaFrame::nueva().await; async_system::reloj::EsperaFrame::nueva().await;
compositor::atender_mandos(); 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 /// del escritorio del compositor y despacha la app como tarea cooperativa del
/// reactor. Si el bytecode falta, esta corrupto, o la carga fracasa, el /// reactor. Si el bytecode falta, esta corrupto, o la carga fracasa, el
/// compositor desaloja esa ventana — el kernel sigue con las demas. /// 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 // 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. // dicta su `EntradaApp`; el compositor decide en que marco lo coloca.
let natural = manifiesto::region(entrada); 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, Ok(Some(objeto)) => objeto.datos,
_ => { _ => {
compositor::desalojar(indice, Color::DESALOJO); compositor::desalojar(indice, Color::DESALOJO);
return; return None;
} }
}; };
// `indice` es la identidad de la app: su ventana en el escritorio del // `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)), Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
Err(_) => compositor::desalojar(indice, Color::DESALOJO), 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 /// 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::fundar(ancho_pantalla, alto_pantalla, &naturales);
compositor::componer_escenario(); compositor::componer_escenario();
let mut plantillas: Vec<Plantilla> = Vec::new();
for (indice, entrada) in m.apps.iter().enumerate() { 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 // 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()); ejecutor.spawn(tarea_compositor());
} }
} }