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

El compositor de la 8a teselaba, pero era inmovil. Las 8b/8c lo hacen
vivo: el teclado reordena el escritorio y mueve el foco en caliente.

- Cache de fotogramas: cada ventana guarda en RAM del kernel su ultimo
  fotograma —reservada una vez, acotada al lienzo natural—. Al re-teselar
  o mover el foco, el kernel recompone desde la cache: las apps que solo
  pintan en init (cronista) conservan su imagen sin enterarse del cambio.
- compositor: el registro ESCRITORIO (ventanas, marcos, caches, modo);
  presentar_fotograma, desalojar, atender_mandos, ciclar_layout,
  mover_foco. Foco en un AtomicUsize, mandos en una cola lock-free.
- teclado: la IRQ1 deja de difundir. Alt es el modificador del sistema —
  Alt+Espacio cicla el teselado, Alt+J/K mueven el foco—; una tecla
  ordinaria va SOLO a la app enfocada (CANALES reindexado por indice_app).
- consola: borde de foco (indigo / gris) en cada marco.

Guardarrail anti-interbloqueo: la IRQ1 jamas bloquea ESCRITORIO; se
comunica por dos atomicos y una cola lock-free. Las caches se reservan
una sola vez, al tamaño natural — sin asignacion en el bucle del reactor.

Verificado en QEMU (screendump + sendkey): arranque teselado con hola
enfocada; Alt+Espacio cicla a CenteredMaster y las apps estaticas
conservan su contenido; Alt+J mueve el foco; las teclas llegan solo a la
app enfocada. Cierra la Fase 8 — el compositor teselante e interactivo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 19:19:21 +00:00
parent e94023d8af
commit 5c462e6d30
12 changed files with 676 additions and 177 deletions
+59
View File
@@ -672,3 +672,62 @@ COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA con
centrado en su panel; las apps desalojadas (discola, glotona) tiñen su marco
entero con la baliza. El registro de arranque permanece legible en la franja
superior de la consola.
## Fases 8b y 8c — El escritorio interactivo — 2026-05-22
El compositor de la Fase 8a teselaba, pero era inmóvil. Las Fases 8b y 8c lo
hacen VIVO: el teclado reordena el escritorio en caliente y mueve el foco. Y
para que las apps estáticas no se queden atrás en una reordenación, el kernel
asume la persistencia visual con una caché de fotogramas.
### Añadido
- **Caché de fotogramas (backbuffer).** Cada ventana del compositor guarda, en
RAM del kernel, el último fotograma que su app envió. La caché se reserva UNA
vez, acotada al lienzo natural de la app —jamás crece—. Al re-teselar o mover
el foco, el kernel recompone cada ventana desde su caché: una app que sólo
pintó en su `init` —la cronista— conserva su imagen intacta a través de
cualquier reordenación, sin enterarse del cambio geométrico.
- **`compositor`: el escritorio interactivo.** Un registro `ESCRITORIO` —las
ventanas, sus marcos, sus cachés, el modo de teselado—; `presentar_fotograma`
(cachea y compone), `desalojar`, `atender_mandos`, `ciclar_layout`,
`mover_foco`. El foco vive en un `AtomicUsize`; los mandos del teclado, en
una cola lock-free.
- **`tarea_compositor`** — atiende los mandos del teclado en cada fotograma del
reactor: el contexto cooperativo donde es seguro re-teselar y recomponer.
- `Color::FOCO` (índigo brillante) y `Color::SIN_FOCO` (gris mate) — el borde
de cada ventana, que delata de un vistazo quién tiene el foco.
### Cambiado
- **Teclado (Fase 8c).** El manejador de IRQ1 deja de difundir a ciegas:
- La tecla **Alt** es el modificador del sistema. `Alt+Espacio` cicla el
modo de teselado; `Alt+J` / `Alt+K` mueven el foco. Estos mandos se
consumen en la IRQ — jamás llegan a una app.
- Una tecla ordinaria se entrega SÓLO a la app ENFOCADA. El censo de canales
se reindexó por `indice_app` (`Vec<Option<CanalTeclado>>`): el foco —un
índice— elige el canal exacto.
- `consola::volcar_marco` y `pintar_desalojo` trazan el borde de foco de la
ventana. `sys_render_frame` ya no compone directo: delega en
`compositor::presentar_fotograma`, que cachea antes de componer.
- `AplicacionWasm` / `ContextoCapacidades` pierden el `marco` —ahora vive en el
registro del compositor, mutable—; conservan sólo el `indice_app`.
### Exclusión de interrupciones (guardarraíl)
- El registro `ESCRITORIO` lo tocan SÓLO tareas cooperativas; el manejador de
IRQ1 jamás lo bloquea. La IRQ se comunica con el mundo cooperativo por un
canal estrecho y a prueba de interbloqueos: dos atómicos —el foco y el estado
de Alt— y una cola lock-free de mandos. Ningún cerrojo en disputa entre la
interrupción y una tarea.
- Las cachés se reservan en `compositor::fundar`, una sola vez, al tamaño
natural de cada app: ninguna asignación dinámica dentro del bucle del reactor.
### Verificado
- QEMU (captura headless + `sendkey` por el monitor):
- **Arranque** — cinco apps teseladas en `MasterStack`; `hola` enfocada luce
el borde índigo, las demás el gris.
- **`Alt+Espacio`** — el escritorio cicla a `CenteredMaster`; discola,
glotona y cronista conservan su contenido a través del re-teselado: la
caché de fotogramas en acción.
- **`Alt+J`** — el foco salta de `hola` a `memoriosa`; el borde índigo se
mueve con él.
- **Enrutamiento** — con `memoriosa` enfocada, cuatro pulsaciones llegan sólo
a ella —cuatro celdas violetas—; las demás apps, intactas.
+5 -5
View File
@@ -73,11 +73,11 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
## Estado
Fases 1 a 5, 6.0, 6.1, 6.2 y la Fase 7 COMPLETA —el userspace nace del grafo
de objetos: Manifiesto de Génesis (7a), imagen sembrada por `boot` (7b) y
persistencia inter-sesión por-app (7c)—. Fase 8 en curso: 8a hecha —el
compositor tesela las ventanas con `mirada-layout`—. Todo verificado en QEMU.
Ver `ROADMAP.md`.
Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de
objetos— y la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
con `mirada-layout` (8a), `Alt+Espacio` cicla el layout (8b), foco con
`Alt+J`/`Alt+K` y enrutamiento selectivo del teclado (8c)—. Todo verificado en
QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo
+30
View File
@@ -342,6 +342,36 @@ de siempre; es el arquitecto quien decide en qué pared colgarlo, y la casa
quien lo centra con cuidado en el espacio que le tocó. Nadie tuvo que cambiar
para vivir mejor repartido.
## La casa atiende
El arquitecto sabía repartir los cuartos, pero su plano era de piedra: una vez
puesto, no se movía. Hoy la casa aprendió a obedecer. Quien la habita puede
pedirle, con un gesto del teclado, que reordene las habitaciones —de una
disposición con una pieza grande a un lado a otra con la pieza central—; y el
arquitecto, en un parpadeo, las reparte de nuevo.
Pero reordenar tiene un peligro. Algunos inquilinos pintan su cuadro una sola
vez, al llegar, y luego se quedan quietos —la cronista es así—. Si la casa
moviera su cuarto, la pared nueva quedaría en blanco: el inquilino, dormido, no
sabría que debe volver a pintar. La casa lo resolvió con delicadeza: de cada
cuarto guarda una fotografía del último cuadro. Cuando reordena, no despierta a
nadie; descuelga las fotografías y las vuelve a colgar, ya encuadradas, en las
paredes nuevas. El que dormía sigue durmiendo, y su obra reaparece intacta.
Y la casa aprendió, además, a mirar. Ahora hay siempre un cuarto ENFOCADO —el
que tiene la atención—, y se le reconoce por un marco de luz índigo; los demás
llevan un marco gris, discreto. Con dos teclas se pasea esa luz de un inquilino
a otro. No es un adorno: es una decisión. Porque desde hoy, cuando alguien
teclea, sus palabras ya no se gritan a toda la casa —como hasta ayer—; se
entregan, en privado, sólo al inquilino que tiene el foco. Se le habla a quien
se mira.
Detrás de esa cortesía hay una disciplina estricta. La campanilla del teclado
—la interrupción— es impaciente y no puede esperar a nadie; por eso jamás toca
los libros de la casa. Sólo deja una nota breve en un casillero a prueba de
prisas, y sigue su camino. Es el arquitecto quien, más tarde y con calma, lee
la nota y mueve los tabiques. Nadie se pisa; nada se traba.
---
*El diario continúa. La próxima página la escribirá la próxima jornada.*
+20 -12
View File
@@ -29,21 +29,29 @@ bare-metal.
- `EntradaApp.region_ancho/alto` pasa a significar el tamaño NATURAL del lienzo
de la app; `region_x/y` quedan vestigiales — el compositor decide la posición.
### 8b — Teselado interactivo
### 8b — Teselado interactivo — ✅ HECHA
- El teclado cicla en caliente los siete modos de `mirada-layout`
(`MasterStack`, `CenteredMaster`, `Spiral`, `Grid`, `Columns`, `Rows`,
`Monocle`).
- Re-teselar exige re-componer: las apps que sólo pintan en `init` necesitan
una señal de redibujado, o el kernel debe conservar su último fotograma para
recomponerlo en el marco nuevo.
- `Alt+Espacio` cicla en caliente los siete modos de teselado de
`mirada-layout` (`MasterStack`, `CenteredMaster`, `Spiral`, …).
- Re-teselar exige re-componer. La solución: el kernel guarda una CACHÉ del
último fotograma de cada app —acotada al lienzo natural, reservada una sola
vez— y recompone desde ella. Una app que sólo pintó en su `init` conserva su
imagen a través de cualquier reordenación, sin enterarse del cambio.
### 8c — Foco y `Workspace`
### 8c — Foco y enrutamiento selectivo — ✅ HECHA
- Adoptar el `Workspace` de `mirada-layout`: foco, orden-Z, alta y baja de
ventanas en vivo.
- Una ventana enfocada, resaltada; el teclado mueve el foco y promueve apps al
área maestra.
- Una ventana ENFOCADA, con borde índigo brillante; las demás, borde gris
mate. `Alt+J` / `Alt+K` pasean el foco entre las ventanas vivas.
- El teclado deja de difundir a ciegas: una tecla ordinaria se entrega sólo a
la app enfocada; los mandos `Alt+…` se consumen en la IRQ.
- El registro `ESCRITORIO` del compositor cumple el papel del `Workspace` de
`mirada-layout` —ventanas, marcos, foco— sin adoptar su tipo: el kernel
necesita además la caché de respaldo, que el `Workspace` no contempla.
### Pendiente
- Orden-Z y solapamiento (ventanas flotantes); alta y baja de apps en vivo.
- Promover la app enfocada al área maestra (`Alt+Enter`).
## Estructura de archivos
+10 -6
View File
@@ -120,7 +120,7 @@ destierra — las aplicaciones pasan a ser objetos del grafo, gobernadas por un
y la app retoma donde quedó. Capacidades `sys_estado_cargar` /
`sys_estado_guardar`; el kernel custodia un manifiesto VIVO y mutable.
## Fase 8 — el compositor teselante
## Fase 8 — el compositor teselante e interactivo (completada)
El kernel deja de colocar las ventanas a mano: las **tesela**. El motor es
`mirada-layout` —el mismo núcleo `no_std` que ordena el compositor Wayland de
@@ -132,12 +132,16 @@ en `FASE8.md`.
de cada app dentro de su marco; las apps no cambian una instrucción.
`region_x/y` del manifiesto quedan vestigiales — la posición la decide el
compositor.
- **8b — teselado interactivo (pendiente).** El teclado cicla en caliente los
siete modos de teselado de `mirada-layout`.
- **8c — foco y `Workspace` (pendiente).** Foco, orden-Z y alta/baja de
ventanas en vivo, con el `Workspace` de `mirada-layout`.
- **8b — teselado interactivo (completada).** `Alt+Espacio` cicla los modos de
teselado en caliente. El kernel cachea el último fotograma de cada app y
recompone desde la caché: las apps estáticas sobreviven al re-teselado sin
enterarse del cambio.
- **8c — foco y enrutamiento selectivo (completada).** Una ventana enfocada,
con borde índigo; `Alt+J` / `Alt+K` mueven el foco entre las ventanas vivas.
El teclado deja de difundir: entrega cada tecla sólo a la app enfocada.
Líneas abiertas posteriores: más capacidades del host (temporización, audio).
Líneas abiertas posteriores: orden-Z y ventanas flotantes; más capacidades del
host (temporización, audio).
## Principios que persisten entre fases
+97 -29
View File
@@ -1,16 +1,28 @@
// =============================================================================
// renaser :: async_system/teclado.rs — el canal de scancodes del teclado
// -----------------------------------------------------------------------------
// El manejador de IRQ1 es un mero PRODUCTOR: deposita cada scancode en colas
// El manejador de IRQ1 es el PRODUCTOR: deposita cada scancode en colas
// lock-free, seguras frente a interrupciones. Los consumidores —las apps WASM,
// via la capacidad `sys_get_scancode`— las drenan sin bloquear.
//
// FASE 5 :: con varias apps concurrentes, una sola cola compartida no sirve:
// la primera en sondear le robaria la pulsacion a las demas. Por eso cada
// aplicacion abre su PROPIO canal y la IRQ1 DIFUNDE cada scancode a todos —
// cada app recibe su copia integra del flujo de entrada.
// FASE 5 :: cada app abre su PROPIO canal; la primera en sondear no le roba la
// pulsacion a las demas.
//
// FASE 8c :: el teclado deja de DIFUNDIR a ciegas. Ahora discrimina:
//
// * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make
// codes son MANDOS del compositor (ciclar el teselado, mover el foco): se
// consumen aqui, jamas llegan a una app.
// * Una tecla ordinaria se entrega SOLO a la app ENFOCADA — la que el
// compositor senala. El censo de canales se indexa por el `indice_app`,
// de modo que el foco —un atomico— elija el canal exacto.
//
// Todo esto corre en contexto de IRQ y NO bloquea ningun cerrojo cooperativo:
// el modificador es un atomico, los mandos van a una cola lock-free.
// =============================================================================
use core::sync::atomic::{AtomicBool, Ordering};
use alloc::sync::Arc;
use alloc::vec::Vec;
@@ -18,16 +30,33 @@ use crossbeam_queue::ArrayQueue;
use spin::{Mutex, Once};
use x86_64::instructions::interrupts;
use crate::compositor::{self, Mando};
/// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto.
const CAPACIDAD_COLA: usize = 256;
// --- Scancodes del set 1 que el teclado interpreta como mandos del sistema. ---
/// Alt izquierda — make (pulsada) y break (soltada).
const ALT_MAKE: u8 = 0x38;
const ALT_BREAK: u8 = 0xB8;
/// Barra espaciadora — `Alt + Espacio` cicla el modo de teselado.
const ESPACIO: u8 = 0x39;
/// Tecla J — `Alt + J` mueve el foco a la ventana siguiente.
const TECLA_J: u8 = 0x24;
/// Tecla K — `Alt + K` mueve el foco a la ventana anterior.
const TECLA_K: u8 = 0x25;
/// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
pub type CanalTeclado = Arc<ArrayQueue<u8>>;
/// Censo de canales — uno por aplicacion del userspace. El manejador de IRQ1
/// difunde cada scancode a TODOS: asi cada app recibe su propia copia del
/// evento, sin que una le arrebate la pulsacion a otra.
static CANALES: Once<Mutex<Vec<CanalTeclado>>> = Once::new();
/// Censo de canales, INDEXADO por el `indice_app` de cada aplicacion. Una
/// ranura `None` es una app que no abrio canal o que fue desalojada. El
/// indexado estable permite que el foco —un simple indice— elija el canal.
static CANALES: Once<Mutex<Vec<Option<CanalTeclado>>>> = Once::new();
/// ¿Esta la tecla Alt pulsada? El modificador de los mandos del sistema. Lo
/// escribe y lo lee SOLO el manejador de IRQ1 — un atomico, sin cerrojo.
static ALT_PULSADO: AtomicBool = AtomicBool::new(false);
/// Funda el censo de canales del teclado. Requiere el heap ya activo; debe
/// invocarse una sola vez, antes de habilitar las interrupciones.
@@ -35,40 +64,79 @@ pub fn init() {
CANALES.call_once(|| Mutex::new(Vec::new()));
}
/// Crea un canal de teclado nuevo, AUN sin inscribir en la difusion. Cada
/// Crea un canal de teclado nuevo, AUN sin inscribir en el censo. Cada
/// aplicacion reclama el suyo al empezar a cargarse.
pub fn crear_canal() -> CanalTeclado {
Arc::new(ArrayQueue::new(CAPACIDAD_COLA))
}
/// Inscribe un canal en el censo de difusion. Desde este instante, la IRQ1
/// empuja cada scancode tambien a este canal. Se invoca al final de la carga
/// de una app: una carga fallida no debe dejar canales huerfanos.
pub fn registrar_canal(canal: &CanalTeclado) {
/// Inscribe el canal de la app `indice` en el censo. Desde este instante, una
/// tecla ordinaria llega a esta app cuando tiene el foco. Se invoca al final de
/// la carga de una app: una carga fallida no debe dejar canales huerfanos.
pub fn registrar_canal(indice: usize, canal: &CanalTeclado) {
if let Some(censo) = CANALES.get() {
// El cerrojo lo disputa el manejador de IRQ1: tomarlo con las
// interrupciones acalladas hace imposible el interbloqueo.
interrupts::without_interrupts(|| censo.lock().push(canal.clone()));
}
}
/// Da de baja un canal del censo de difusion. Lo invoca el `Drop` de una
/// aplicacion desalojada: la IRQ1 deja, de inmediato, de empujarle scancodes.
pub fn cerrar_canal(canal: &CanalTeclado) {
if let Some(censo) = CANALES.get() {
interrupts::without_interrupts(|| {
censo.lock().retain(|inscrito| !Arc::ptr_eq(inscrito, canal));
let mut censo = censo.lock();
while censo.len() <= indice {
censo.push(None);
}
censo[indice] = Some(canal.clone());
});
}
}
/// Punto de entrada DESDE el manejador de IRQ1. DIFUNDE el scancode a cuantos
/// canales haya abiertos. Deliberadamente breve y libre de panicos: corre en
/// contexto de interrupcion.
pub fn recibir_scancode(scancode: u8) {
/// Da de baja el canal de la app `indice`. Lo invoca el `Drop` de una
/// aplicacion desalojada: la ranura queda en `None` y la IRQ deja de enrutarle
/// teclas, sin desplazar los indices de las demas.
pub fn cerrar_canal(indice: usize) {
if let Some(censo) = CANALES.get() {
for canal in censo.lock().iter() {
// Si un canal desborda, se descarta el scancode en silencio: mas
interrupts::without_interrupts(|| {
let mut censo = censo.lock();
if let Some(ranura) = censo.get_mut(indice) {
*ranura = None;
}
});
}
}
/// Punto de entrada DESDE el manejador de IRQ1. Rastrea el modificador Alt,
/// intercepta los mandos del sistema y enruta la tecla ordinaria a la app
/// enfocada. Deliberadamente breve y libre de panicos: corre en contexto de
/// interrupcion y no bloquea ningun cerrojo cooperativo.
pub fn recibir_scancode(scancode: u8) {
// 1. Rastrear la tecla Alt — el modificador de los mandos del sistema. Se
// consume: el modificador nunca se difunde a una app.
match scancode {
ALT_MAKE => {
ALT_PULSADO.store(true, Ordering::Relaxed);
return;
}
ALT_BREAK => {
ALT_PULSADO.store(false, Ordering::Relaxed);
return;
}
_ => {}
}
// 2. Con Alt pulsada, los make codes son MANDOS del compositor. Se traducen
// a una orden en la cola lock-free y se consumen — jamas llegan a una app.
if ALT_PULSADO.load(Ordering::Relaxed) {
match scancode {
ESPACIO => compositor::solicitar(Mando::CiclarLayout),
TECLA_J => compositor::solicitar(Mando::FocoSiguiente),
TECLA_K => compositor::solicitar(Mando::FocoAnterior),
_ => {}
}
return;
}
// 3. Tecla ordinaria: se entrega SOLO a la app que tiene el foco. El foco
// es un indice atomico; el censo, un vector indexado por `indice_app`.
if let Some(censo) = CANALES.get() {
if let Some(Some(canal)) = censo.lock().get(compositor::foco()) {
// Si el canal desborda, se descarta el scancode en silencio: mas
// vale perder una tecla que colapsar dentro de una interrupcion.
let _ = canal.push(scancode);
}
+307 -27
View File
@@ -1,41 +1,323 @@
// =============================================================================
// renaser :: kernel/src/compositor.rs — Fase 8 :: el compositor teselante
// -----------------------------------------------------------------------------
// Hasta la Fase 7, cada app llevaba su region escrita a mano en el manifiesto:
// coordenadas fijas, una composicion rigida. La Fase 8 entrega esa decision a
// un COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA.
// El kernel no coloca las ventanas a mano: las TESELA. El motor es
// `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
// de brahman—, enlazado por `path` cruzando la frontera de workspace.
//
// El motor de teselado es `mirada-layout` el mismo nucleo `no_std` que
// ordena las ventanas del compositor Wayland de brahman. Cruza la frontera de
// workspace y se enlaza aqui sin una linea de codigo nueva: geometria pura,
// determinista, la misma en Linux y en el bare-metal de renaser.
// FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
// de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
// fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
// caliente —o mover el foco— y el kernel recompone cada ventana en su marco
// nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
// su imagen intacta a traves de cualquier reordenacion.
//
// Cada app conserva su tamaño NATURAL —el lienzo que sabe pintar, fijo—; el
// compositor decide DONDE va ese lienzo. El kernel centra el fotograma natural
// de la app dentro del marco teselado. Asi el compositor reordena la pantalla
// sin que ninguna app cambie una sola instruccion.
// EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
// cooperativas (el `tick` de una app, la tarea del compositor): el manejador
// de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
// un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
// estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
// pudiera disputar a una tarea cooperativa.
// =============================================================================
use core::sync::atomic::{AtomicUsize, Ordering};
use alloc::vec;
use alloc::vec::Vec;
use crossbeam_queue::ArrayQueue;
use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
use spin::{Mutex, Once};
use crate::grafico::RegionPantalla;
use crate::grafico::{Color, RegionPantalla};
/// Altura del strip superior reservado a la consola; las apps teselan debajo.
/// La consola conserva ahi su registro de arranque completo —seis lineas,
/// hasta la sonda asincrona de disco— legible sobre el teselado.
const FRANJA_CONSOLA: usize = 296;
/// El modo de teselado del compositor. Fijo por ahora — la Fase 8b lo hara
/// conmutable en caliente desde el teclado, recorriendo los siete modos que
/// `mirada-layout` ofrece.
const MODO: LayoutMode = LayoutMode::MasterStack;
/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
/// Margen entre ventanas teseladas, en pixeles — el aire que separa un marco
/// de sus vecinos.
/// Margen entre ventanas teseladas, en pixeles.
const MARGEN: i32 = 14;
/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
const CAPACIDAD_MANDOS: usize = 32;
/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
/// atiende la tarea del compositor desde el reactor cooperativo.
#[derive(Clone, Copy)]
pub enum Mando {
/// Ciclar al siguiente modo de teselado de `mirada-layout`.
CiclarLayout,
/// Mover el foco a la siguiente ventana viva.
FocoSiguiente,
/// Mover el foco a la ventana viva anterior.
FocoAnterior,
}
/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
struct Ventana {
/// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
natural_ancho: usize,
natural_alto: usize,
/// El marco teselado actual — donde la app vive en pantalla. Cambia con
/// cada re-teselado.
marco: RegionPantalla,
/// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
/// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
/// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
/// re-teselado recompone la ventana desde aqui, sin molestar a la app.
cache: Vec<u8>,
/// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
/// solo ceros y no se recompone.
pintada: bool,
/// Si el kernel desalojo la app, el color de su baliza. `None` mientras
/// vive; `Some(color)` la marca como muerta y la excluye del foco.
baliza: Option<Color>,
}
/// El escritorio: el registro de todas las ventanas y el modo de teselado.
/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
struct Escritorio {
modo: LayoutMode,
ancho: usize,
alto: usize,
ventanas: Vec<Ventana>,
}
/// El escritorio global. Se funda una sola vez, en el arranque.
static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
static FOCO: AtomicUsize = AtomicUsize::new(0);
/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
/// compositor las drena desde el reactor cooperativo.
static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
// =============================================================================
// Fundacion y consulta — el arranque
// =============================================================================
/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
let marcos = teselar(naturales.len(), ancho, alto, MODO_INICIAL);
let mut ventanas = Vec::with_capacity(naturales.len());
for (i, &(nat_ancho, nat_alto)) in naturales.iter().enumerate() {
ventanas.push(Ventana {
natural_ancho: nat_ancho,
natural_alto: nat_alto,
marco: marcos[i],
// La cache: reservada UNA vez, acotada al lienzo natural.
cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
pintada: false,
baliza: None,
});
}
ESCRITORIO.call_once(|| {
Mutex::new(Escritorio {
modo: MODO_INICIAL,
ancho,
alto,
ventanas,
})
});
}
/// Pinta el escenario inicial del compositor: el area de apps y sus marcos
/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps.
pub fn componer_escenario() {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let escritorio = escritorio.lock();
let area = area_apps(escritorio.ancho, escritorio.alto);
let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
crate::consola::pintar_escenario(area, &marcos);
}
/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
pub fn foco() -> usize {
FOCO.load(Ordering::Relaxed)
}
/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
/// cola lock-free es seguro en contexto de interrupcion.
pub fn solicitar(mando: Mando) {
if let Some(mandos) = MANDOS.get() {
// Si la cola se desborda, el mando se pierde en silencio: mas vale
// perder una pulsacion que arriesgar un panico dentro de una IRQ.
let _ = mandos.push(mando);
}
}
// =============================================================================
// El fotograma de una app — cache y composicion
// =============================================================================
/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
/// kernel asume la persistencia visual— y lo compone, centrado, en su marco.
/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo.
pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let (marco, nat_ancho, nat_alto) = {
let mut escritorio = escritorio.lock();
let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return;
};
// Cachear el fotograma. El destino esta acotado al lienzo natural; se
// copia el minimo de ambas longitudes — jamas se desborda la cache.
let n = ventana.cache.len().min(datos.len());
ventana.cache[..n].copy_from_slice(&datos[..n]);
ventana.pintada = true;
(ventana.marco, ventana.natural_ancho, ventana.natural_alto)
};
let enfocada = FOCO.load(Ordering::Relaxed) == indice;
crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
}
/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
/// Desde aqui queda excluida del foco — el teclado la salta.
pub fn desalojar(indice: usize, color: Color) {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let marco = {
let mut escritorio = escritorio.lock();
let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return;
};
ventana.baliza = Some(color);
ventana.marco
};
let enfocada = FOCO.load(Ordering::Relaxed) == indice;
crate::consola::pintar_desalojo(marco, color, enfocada);
}
// =============================================================================
// Los mandos del teclado — el escritorio interactivo
// =============================================================================
/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
/// seguro bloquear el `ESCRITORIO` y la consola.
pub fn atender_mandos() {
let Some(mandos) = MANDOS.get() else {
return;
};
while let Some(mando) = mandos.pop() {
match mando {
Mando::CiclarLayout => ciclar_layout(),
Mando::FocoSiguiente => mover_foco(true),
Mando::FocoAnterior => mover_foco(false),
}
}
}
/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las
/// ventanas y recompone el escritorio entero desde las caches de respaldo.
fn ciclar_layout() {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let mut escritorio = escritorio.lock();
escritorio.modo = escritorio.modo.next();
let marcos = teselar(
escritorio.ventanas.len(),
escritorio.ancho,
escritorio.alto,
escritorio.modo,
);
for (ventana, marco) in escritorio.ventanas.iter_mut().zip(marcos) {
ventana.marco = marco;
}
redibujar_todo(&escritorio);
}
/// Mueve el foco a la siguiente ventana VIVA —saltando las desalojadas—; tras
/// el salto, redibuja la ventana que pierde el foco y la que lo gana, para que
/// el borde de cada una cambie de color.
fn mover_foco(adelante: bool) {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let escritorio = escritorio.lock();
let n = escritorio.ventanas.len();
if n == 0 {
return;
}
let anterior = FOCO.load(Ordering::Relaxed).min(n - 1);
// Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
// `n` pasos se vuelve al punto de partida y el foco no cambia.
let mut nuevo = anterior;
for _ in 0..n {
nuevo = if adelante {
(nuevo + 1) % n
} else {
(nuevo + n - 1) % n
};
if escritorio.ventanas[nuevo].baliza.is_none() {
break;
}
}
FOCO.store(nuevo, Ordering::Relaxed);
redibujar_ventana(&escritorio.ventanas[anterior], false);
redibujar_ventana(&escritorio.ventanas[nuevo], true);
}
/// Recompone el escritorio entero: repinta el escenario —area y paneles— con
/// los marcos nuevos y, sobre el, cada ventana desde su cache de respaldo.
fn redibujar_todo(escritorio: &Escritorio) {
let area = area_apps(escritorio.ancho, escritorio.alto);
let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
crate::consola::pintar_escenario(area, &marcos);
let foco = FOCO.load(Ordering::Relaxed);
for (i, ventana) in escritorio.ventanas.iter().enumerate() {
redibujar_ventana(ventana, i == foco);
}
}
/// Redibuja UNA ventana en su marco actual: si fue desalojada, su baliza; si ya
/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel
/// del escenario ya esta puesto—.
fn redibujar_ventana(ventana: &Ventana, enfocada: bool) {
match ventana.baliza {
Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada),
None => {
if ventana.pintada {
crate::consola::volcar_marco(
ventana.marco,
ventana.natural_ancho,
ventana.natural_alto,
&ventana.cache,
enfocada,
);
}
}
}
}
// =============================================================================
// Teselado — la geometria pura de `mirada-layout`
// =============================================================================
/// El area de pantalla que el compositor tesela: toda la pantalla menos la
/// franja de la consola en la cima.
pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
@@ -47,11 +329,10 @@ pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla
}
}
/// Tesela el area de apps en `n` marcos —uno por ventana, en el orden de las
/// apps del manifiesto— con el algoritmo de `mirada-layout`. El vector
/// resultante tiene exactamente `n` elementos.
pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<RegionPantalla> {
let area = area_apps(ancho_pantalla, alto_pantalla);
/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
/// tiene exactamente `n` elementos, en el orden de las apps del manifiesto.
fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
let area = area_apps(ancho, alto);
let pantalla = Rect::new(
area.x as i32,
area.y as i32,
@@ -59,7 +340,7 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<Re
area.alto as i32,
);
let params = LayoutParams {
mode: MODO,
mode: modo,
gap: MARGEN,
..LayoutParams::default()
};
@@ -69,9 +350,8 @@ pub fn disponer(n: usize, ancho_pantalla: usize, alto_pantalla: usize) -> Vec<Re
.collect()
}
/// Traduce un `Rect` de `mirada-layout` (`i32`, en teoria con signo) a la
/// `RegionPantalla` del kernel (`usize`). Un rectangulo degenerado queda en
/// cero — el kernel no compondra nada en el.
/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
/// kernel (`usize`). Un rectangulo degenerado queda en cero.
fn rect_a_region(r: Rect) -> RegionPantalla {
RegionPantalla {
x: r.x.max(0) as usize,
+50 -17
View File
@@ -130,6 +130,7 @@ impl Consola {
nat_ancho: usize,
nat_alto: usize,
datos: &[u8],
enfocada: bool,
) {
if nat_ancho == 0 || nat_alto == 0 {
return;
@@ -165,17 +166,47 @@ impl Consola {
self.lienzo.pixeles[y * self.lienzo.ancho + x] =
codificar(self.lienzo.formato, color);
}
// El borde del compositor: delata, de un vistazo, quien tiene el foco.
self.dibujar_borde(marco, enfocada);
self.presentar();
}
/// Inunda una region entera con un color plano y la presenta. Es la baliza
/// de desalojo: cuando una aplicacion falla, su marco se tatua de purpura.
fn pintar_region(&mut self, region: RegionPantalla, color: Color) {
/// Inunda una region entera con un color plano la baliza de desalojo: una
/// app que falla tatua su marco de purpura— y le traza su borde de foco.
fn pintar_region(&mut self, region: RegionPantalla, color: Color, enfocada: bool) {
self.lienzo
.rellenar_rect(region.x, region.y, region.ancho, region.alto, color);
self.dibujar_borde(region, enfocada);
self.presentar();
}
/// Traza un borde de 3 px alrededor de un marco: indigo brillante si la
/// ventana tiene el foco del compositor, gris mate si no (Fase 8c).
fn dibujar_borde(&mut self, marco: RegionPantalla, enfocada: bool) {
const GROSOR: usize = 3;
let color = if enfocada { Color::FOCO } else { Color::SIN_FOCO };
// Lados superior e inferior.
self.lienzo
.rellenar_rect(marco.x, marco.y, marco.ancho, GROSOR, color);
self.lienzo.rellenar_rect(
marco.x,
marco.y + marco.alto.saturating_sub(GROSOR),
marco.ancho,
GROSOR,
color,
);
// Lados izquierdo y derecho.
self.lienzo
.rellenar_rect(marco.x, marco.y, GROSOR, marco.alto, color);
self.lienzo.rellenar_rect(
marco.x + marco.ancho.saturating_sub(GROSOR),
marco.y,
GROSOR,
marco.alto,
color,
);
}
/// Pinta el escenario del compositor (Fase 8): inunda el area de apps con
/// el reposo del lienzo —borrando cuanto hubiera debajo— y, sobre ella,
/// tiñe cada marco teselado con el color de panel. Asi el teselado se ve
@@ -205,36 +236,38 @@ impl Consola {
/// asincronas y las capacidades del userspace escriben en ella tras su `Mutex`.
pub(crate) static CONSOLA: Once<Mutex<Consola>> = Once::new();
/// Puerta del kernel para la capacidad `sys_render_frame` del userspace WASM:
/// compone sobre la consola global un fotograma —cuyos limites el host ya
/// verifico matematicamente contra la memoria lineal del modulo— centrado en
/// el marco que el compositor asigno a esa aplicacion. `nat_ancho`/`nat_alto`
/// son el tamaño natural del lienzo de la app.
pub(crate) fn volcar_marco_wasm(
/// Compone un fotograma del userspace —ya cacheado por el compositor— centrado
/// en su marco teselado, con su borde de foco. La invoca `compositor` al
/// recibir un `sys_render_frame` y al recomponer el escritorio tras un mando.
pub(crate) fn volcar_marco(
marco: RegionPantalla,
nat_ancho: usize,
nat_alto: usize,
datos: &[u8],
enfocada: bool,
) {
if let Some(consola) = CONSOLA.get() {
consola.lock().volcar_marco(marco, nat_ancho, nat_alto, datos);
consola
.lock()
.volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
}
}
/// Pinta el escenario del compositor (Fase 8): el area de apps y, sobre ella,
/// cada marco teselado. Se invoca una vez, en el arranque, tras teselar.
/// cada marco teselado. La invoca `compositor` al arrancar y al re-teselar.
pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) {
if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_escenario(area, marcos);
}
}
/// Tatua la baliza de desalojo sobre la region de una aplicacion que el kernel
/// ha dado por terminada. El color delata la causa —purpura para una falla de
/// ejecucion o de combustible, amarillo palido para un desbordo de memoria—. Es
/// una advertencia NO fatal: la app muere, el kernel y sus vecinas siguen vivos.
pub(crate) fn pintar_desalojo(region: RegionPantalla, color: Color) {
/// Tatua la baliza de desalojo sobre el marco de una aplicacion que el kernel
/// ha dado por terminada, con su borde de foco. El color delata la causa
/// —purpura para una falla de ejecucion o de combustible, amarillo palido para
/// un desbordo de memoria—. Es una advertencia NO fatal: la app muere, el
/// kernel y sus vecinas siguen vivos.
pub(crate) fn pintar_desalojo(marco: RegionPantalla, color: Color, enfocada: bool) {
if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_region(region, color);
consola.lock().pintar_region(marco, color, enfocada);
}
}
+16
View File
@@ -52,6 +52,22 @@ impl Color {
b: 0x30,
};
/// Borde de la ventana ENFOCADA (Fase 8c): un indigo brillante. Delata, de
/// un vistazo, a quien recibe el teclado.
pub(crate) const FOCO: Color = Color {
r: 0x4B,
g: 0x00,
b: 0x82,
};
/// Borde de una ventana sin foco (Fase 8c): un gris mate, discreto — marca
/// el marco sin reclamar la atencion.
pub(crate) const SIN_FOCO: Color = Color {
r: 0x3A,
g: 0x40,
b: 0x4E,
};
/// Alerta de colapso: un rojo saturado, imposible de ignorar.
pub(crate) const ALERTA: Color = Color {
r: 0xD4,
+47 -35
View File
@@ -60,16 +60,16 @@ mod sync;
mod texto;
mod wasm;
// Reexportaciones para que los submodulos conserven rutas `crate::` estables.
pub(crate) use consola::volcar_marco_wasm;
// Reexportacion para que los submodulos conserven rutas `crate::` estables.
pub(crate) use sync::CeldaSync;
use alloc::vec::Vec;
use async_system::executor::Executor;
use baliza::BALIZA_PANICO;
use consola::{Consola, CONSOLA};
use grafico::{
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX,
ANCHO_MAX,
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, ALTO_MAX, ANCHO_MAX,
};
/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU.
@@ -94,20 +94,32 @@ pub(crate) fn detener() -> ! {
/// Tarea cooperativa de una aplicacion WASM. En cada pulso del reloj le concede
/// un `tick` —un fotograma de trabajo— y cede la CPU hasta el siguiente; entre
/// medias corren sus vecinas. Si la app falla o agota su combustible, se la
/// DESALOJA: se tatua su region con la baliza de purpura y la tarea concluye.
/// DESALOJA: el compositor tatua su ventana con la baliza y la tarea concluye.
/// El ejecutor la retira del censo, su memoria se libera, el kernel sigue vivo.
async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
loop {
async_system::reloj::EsperaFrame::nueva().await;
if let Err(falla) = app.tick() {
// El color de la baliza delata la causa: purpura si agoto su tiempo
// o aborto, amarillo si reviento su techo de memoria.
consola::pintar_desalojo(app.marco(), falla.color_baliza());
// o aborto, amarillo si reviento su techo de memoria. El compositor
// la pinta en el marco actual de la ventana y la marca como muerta.
compositor::desalojar(app.indice(), falla.color_baliza());
return;
}
}
}
/// FASE 8 :: la tarea del compositor. En cada fotograma drena la cola de mandos
/// que el teclado dejo —ciclar el modo de teselado, mover el foco— y los
/// aplica. Corre en el reactor cooperativo: el unico contexto donde es seguro
/// re-teselar el escritorio y recomponer el lienzo desde las caches.
async fn tarea_compositor() {
loop {
async_system::reloj::EsperaFrame::nueva().await;
compositor::atender_mandos();
}
}
/// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
/// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las
/// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el
@@ -132,19 +144,13 @@ async fn tarea_sonda_disco() {
}
/// Da vida a una aplicacion del userspace a partir de su `EntradaApp` del
/// manifiesto: recupera su bytecode del grafo, la carga en el `marco` que el
/// compositor le teselo y la despacha como tarea cooperativa del reactor. Si
/// el bytecode falta, esta corrupto, o la carga fracasa, se salda pintando el
/// marco con la baliza de desalojo — el kernel no se inmuta y sigue con las
/// demas.
fn encender_app(
ejecutor: &mut Executor,
indice: usize,
entrada: &manifiesto::EntradaApp,
marco: RegionPantalla,
) {
/// manifiesto: recupera su bytecode del grafo, lo carga en la ventana `indice`
/// del escritorio del compositor y despacha la app como tarea cooperativa del
/// reactor. Si el bytecode falta, esta corrupto, o la carga fracasa, el
/// compositor desaloja esa ventana — el kernel sigue con las demas.
fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::EntradaApp) {
// El tamaño NATURAL del lienzo de la app —lo que sabe pintar, fijo— lo
// dicta su `EntradaApp`; el compositor le asigno `marco` como ventana.
// dicta su `EntradaApp`; el compositor decide en que marco lo coloca.
let natural = manifiesto::region(entrada);
// Recuperar el bytecode del grafo. `recuperar` recomputa el hash del
// objeto y verifica su integridad: un bytecode corrupto se delata aqui
@@ -152,22 +158,21 @@ fn encender_app(
let bytecode = match almacen::recuperar(&entrada.bytecode) {
Ok(Some(objeto)) => objeto.datos,
_ => {
consola::pintar_desalojo(marco, Color::DESALOJO);
compositor::desalojar(indice, Color::DESALOJO);
return;
}
};
// `indice` es la identidad de la app en el manifiesto: las capacidades de
// estado persistido (Fase 7c) la usan para hallar SU ranura `estado`.
// `indice` es la identidad de la app: su ventana en el escritorio del
// compositor y su ranura de estado persistido (Fase 7c).
match wasm::AplicacionWasm::cargar(
&bytecode,
marco,
natural.ancho,
natural.alto,
entrada.techo_memoria as usize,
indice,
) {
Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
Err(_) => consola::pintar_desalojo(marco, Color::DESALOJO),
Err(_) => compositor::desalojar(indice, Color::DESALOJO),
}
}
@@ -218,18 +223,25 @@ fn cargar_userspace(ejecutor: &mut Executor, ancho_pantalla: usize, alto_pantall
// consulta lee del manifiesto vivo.
manifiesto::instalar(m.clone());
// FASE 8 :: el compositor tesela el area de apps — un marco por
// ventana, calculado por `mirada-layout`. Se pinta el escenario (el
// area y sus marcos) antes de encender las apps: el teselado se ve
// aunque alguna app no llegue siquiera a pintar su primer fotograma.
let marcos = compositor::disponer(m.apps.len(), ancho_pantalla, alto_pantalla);
consola::pintar_escenario(
compositor::area_apps(ancho_pantalla, alto_pantalla),
&marcos,
);
for (indice, (entrada, marco)) in m.apps.iter().zip(marcos).enumerate() {
encender_app(ejecutor, indice, entrada, marco);
// FASE 8 :: fundar el escritorio del compositor — una ventana por app,
// con su cache de respaldo y su marco teselado por `mirada-layout`— y
// pintar el escenario antes de encender las apps: el teselado se ve
// aunque alguna app no llegue a pintar su primer fotograma.
let naturales: Vec<(usize, usize)> = m
.apps
.iter()
.map(|e| (e.region_ancho as usize, e.region_alto as usize))
.collect();
compositor::fundar(ancho_pantalla, alto_pantalla, &naturales);
compositor::componer_escenario();
for (indice, entrada) in m.apps.iter().enumerate() {
encender_app(ejecutor, indice, entrada);
}
// La tarea del compositor: atiende los mandos del teclado —ciclar el
// teselado, mover el foco— en cada fotograma del reactor.
ejecutor.spawn(tarea_compositor());
}
}
+10 -12
View File
@@ -29,18 +29,15 @@ use wasmi::{Caller, Error, Extern, Linker, Memory, StoreLimits};
use crate::almacen::Hash;
use crate::async_system::teclado::CanalTeclado;
use crate::grafico::RegionPantalla;
/// El estado del host adscrito al `Store` de una aplicacion: cuanto necesita
/// una capacidad para servir a ESA app y a ninguna otra — su region de pantalla,
/// su canal de teclado y sus cuotas de recursos. Dos apps jamas comparten nada.
pub(crate) struct ContextoCapacidades {
/// El marco que el compositor (Fase 8) asigno a la app — el rectangulo de
/// pantalla donde vive. El kernel centra en el el fotograma de la app.
pub(crate) marco: RegionPantalla,
/// El tamaño natural del lienzo de la app, en pixeles. El fotograma que
/// entrega `sys_render_frame` mide exactamente `natural_ancho × natural_alto`;
/// el compositor lo coloca, sin deformarlo, dentro del `marco`.
/// el compositor lo cachea y lo compone, sin deformarlo, en el marco que el
/// teselado le asigno.
pub(crate) natural_ancho: usize,
pub(crate) natural_alto: usize,
/// El canal de teclado propio de la aplicacion.
@@ -48,9 +45,9 @@ pub(crate) struct ContextoCapacidades {
/// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
/// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
pub(crate) limites: StoreLimits,
/// El indice de esta app en el Manifiesto de Genesis — su identidad. Las
/// capacidades de estado (Fase 7c) lo usan para hallar la `EntradaApp`
/// correcta: cada app persiste en SU ranura, jamas en la de otra.
/// El indice de esta app — su identidad. La usan las capacidades de estado
/// (Fase 7c) para hallar su `EntradaApp` del manifiesto, y el compositor
/// (Fase 8) para hallar su ventana en el escritorio: jamas la de otra.
pub(crate) indice_app: usize,
}
@@ -98,7 +95,7 @@ pub(crate) fn enlazar_capacidades(
"renaser",
"sys_render_frame",
|caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> {
let marco = caller.data().marco;
let indice = caller.data().indice_app;
let nat_ancho = caller.data().natural_ancho;
let nat_alto = caller.data().natural_alto;
@@ -124,9 +121,10 @@ pub(crate) fn enlazar_capacidades(
"WASM :: sys_render_frame desbordo la memoria lineal del modulo",
)?;
// Limites verificados: el compositor centra el fotograma natural
// de la app dentro del marco que el teselado le asigno.
crate::volcar_marco_wasm(marco, nat_ancho, nat_alto, fotograma);
// Limites verificados: el compositor cachea el fotograma —para
// poder recomponerlo si el escritorio se re-tesela— y lo compone,
// centrado, en el marco que el teselado asigno a esta app.
crate::compositor::presentar_fotograma(indice, fotograma);
Ok(())
},
)?;
+25 -34
View File
@@ -20,7 +20,7 @@ use wasmi::{
CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc,
};
use crate::grafico::{Color, RegionPantalla};
use crate::grafico::Color;
use env::ContextoCapacidades;
/// Combustible concedido a `init`. Cubre con holgura el pintado inicial del
@@ -64,34 +64,28 @@ impl FallaApp {
/// aqui la instancia se conserva y el kernel la hace avanzar `tick` a `tick`.
pub struct AplicacionWasm {
/// El almacen: todo el estado de ESTA instancia — su memoria lineal, sus
/// globales y el contexto de capacidades con su region de pantalla.
/// globales y el contexto de capacidades con su identidad e indice.
almacen: Store<ContextoCapacidades>,
/// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
/// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
/// el handle de la `Instance` no aporta nada y no se retiene.
func_tick: TypedFunc<(), ()>,
/// El marco que el compositor asigno a la app — su ventana en pantalla, y
/// donde se tatua su baliza de desalojo si llega a fallar.
marco: RegionPantalla,
}
impl AplicacionWasm {
/// Carga, valida, instancia y arranca una aplicacion WASM aislada, ligada a
/// una region de pantalla. Si algo falla en el camino, se devuelve la falla
/// en lugar de incendiar el kernel.
/// Carga, valida, instancia y arranca una aplicacion WASM aislada. Si algo
/// falla en el camino, se devuelve la falla en lugar de incendiar el kernel.
///
/// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
/// el reactor en cada pulso del reloj.
/// El ABI del userspace exige dos exportaciones: `init` —invocada una sola
/// vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por el
/// reactor en cada pulso del reloj.
///
/// `marco` es el rectangulo que el compositor (Fase 8) asigno a la app;
/// `natural_ancho`/`natural_alto`, el tamaño de su lienzo. `techo_memoria`
/// es su cuota de memoria lineal —la dicta su `EntradaApp` del manifiesto—,
/// e `indice_app` su posicion en el: su identidad para las capacidades de
/// estado persistido (Fase 7c).
/// `natural_ancho`/`natural_alto` son el tamaño del lienzo de la app;
/// `techo_memoria`, su cuota de memoria lineal —la dicta su `EntradaApp` del
/// manifiesto—; e `indice_app`, su identidad: la posicion con que el
/// compositor halla su ventana y las capacidades de estado su ranura.
pub fn cargar(
bytecode: &[u8],
marco: RegionPantalla,
natural_ancho: usize,
natural_alto: usize,
techo_memoria: usize,
@@ -108,10 +102,10 @@ impl AplicacionWasm {
// 2. Validar y traducir el modulo — ya instrumentado con fuel.
let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?;
// 3. El almacen, con el contexto de capacidades de ESTA app: su region
// de pantalla, su canal de teclado y su techo de memoria. El canal
// se crea ahora pero se inscribe en la difusion de la IRQ1 al final,
// ya con la app cargada: una carga fallida no deja canales huerfanos.
// 3. El almacen, con el contexto de capacidades de ESTA app: su lienzo
// natural, su canal de teclado y su techo de memoria. El canal se
// crea ahora pero se inscribe en el censo de la IRQ1 al final, ya con
// la app cargada: una carga fallida no deja canales huerfanos.
let canal = crate::async_system::teclado::crear_canal();
let limites = StoreLimitsBuilder::new()
.memory_size(techo_memoria)
@@ -122,7 +116,6 @@ impl AplicacionWasm {
let mut almacen = Store::new(
&motor,
ContextoCapacidades {
marco,
natural_ancho,
natural_alto,
canal,
@@ -161,14 +154,11 @@ impl AplicacionWasm {
.map_err(|_| FallaApp::Carga)?;
// 8. Con la app ya cargada e instanciada, inscribir su canal de teclado
// en la difusion de la IRQ1: desde aqui recibe cada pulsacion.
crate::async_system::teclado::registrar_canal(&almacen.data().canal);
// en el censo de la IRQ1, en la ranura de su `indice_app`: desde
// aqui recibe las teclas cuando el compositor le da el foco.
crate::async_system::teclado::registrar_canal(indice_app, &almacen.data().canal);
Ok(AplicacionWasm {
almacen,
func_tick,
marco,
})
Ok(AplicacionWasm { almacen, func_tick })
}
/// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de
@@ -194,10 +184,11 @@ impl AplicacionWasm {
}
}
/// El marco de pantalla que el compositor asigno a la aplicacion — donde
/// se tatua su baliza si el kernel llega a desalojarla.
pub fn marco(&self) -> RegionPantalla {
self.marco
/// El indice de la aplicacion — su identidad en el escritorio del
/// compositor. Lo usa la tarea de la app para decirle al compositor que
/// ventana desalojar si la app llega a fallar.
pub fn indice(&self) -> usize {
self.almacen.data().indice_app
}
}
@@ -207,6 +198,6 @@ impl AplicacionWasm {
/// empujando scancodes a una cola muerta: una fuga lenta pero segura.
impl Drop for AplicacionWasm {
fn drop(&mut self) {
crate::async_system::teclado::cerrar_canal(&self.almacen.data().canal);
crate::async_system::teclado::cerrar_canal(self.almacen.data().indice_app);
}
}