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
+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);
}
}