feat(renaser): Fase 8a — el compositor teselante

El kernel deja de colocar 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. Es el
primer consumo REAL del nucleo compartido brahman <-> renaser.

- kernel/compositor.rs: enlaza mirada-layout y calcula un marco por app
  con el algoritmo MasterStack, dentro del area de pantalla.
- consola::volcar_marco centra el fotograma natural de la app dentro de
  su marco teselado (antes lo depositaba en region.x/y fijos).
- ContextoCapacidades lleva marco + natural_ancho/alto; sys_render_frame
  valida el fotograma contra el tamaño natural.
- cargar_userspace tesela con el compositor y pinta el escenario antes
  de encender las apps. Las apps NO cambian: el compositor reordena la
  pantalla sin que ninguna toque una instruccion.

Verificado en QEMU (screendump): las cinco apps de genesis teseladas en
MasterStack — hola como ventana maestra, el resto apiladas a la derecha,
cada lienzo centrado en su panel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 18:56:40 +00:00
parent 8fc26b0c0c
commit 65c88ccf25
11 changed files with 393 additions and 77 deletions
+82
View File
@@ -0,0 +1,82 @@
// =============================================================================
// 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 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.
//
// 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.
// =============================================================================
use alloc::vec::Vec;
use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
use crate::grafico::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;
/// Margen entre ventanas teseladas, en pixeles — el aire que separa un marco
/// de sus vecinos.
const MARGEN: i32 = 14;
/// 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 {
RegionPantalla {
x: 0,
y: FRANJA_CONSOLA.min(alto_pantalla),
ancho: ancho_pantalla,
alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
}
}
/// 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);
let pantalla = Rect::new(
area.x as i32,
area.y as i32,
area.ancho as i32,
area.alto as i32,
);
let params = LayoutParams {
mode: MODO,
gap: MARGEN,
..LayoutParams::default()
};
tile(pantalla, n, &params)
.into_iter()
.map(rect_a_region)
.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.
fn rect_a_region(r: Rect) -> RegionPantalla {
RegionPantalla {
x: r.x.max(0) as usize,
y: r.y.max(0) as usize,
ancho: r.w.max(0) as usize,
alto: r.h.max(0) as usize,
}
}
+71 -17
View File
@@ -119,21 +119,42 @@ impl Consola {
}
/// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
/// sus limites ya verificados por el host— sobre la SUB-REGION asignada a su
/// aplicacion. Cada pixel se recodifica al formato nativo del framebuffer y
/// se deposita desplazado por `(region.x, region.y)`: una app jamas escribe
/// fuera de su ventana, y varias cohabitan el lienzo sin pisarse.
fn volcar_marco(&mut self, region: RegionPantalla, datos: &[u8]) {
/// sus limites ya verificados por el host— dentro del MARCO que el
/// compositor (Fase 8) asigno a su aplicacion. El fotograma mide el tamaño
/// NATURAL de la app (`nat_ancho × nat_alto`); se CENTRA en el marco —que el
/// teselado pudo hacer mayor o menor que ese natural— y se recorta con
/// firmeza a sus bordes. Una app jamas pinta un pixel fuera de su marco.
fn volcar_marco(
&mut self,
marco: RegionPantalla,
nat_ancho: usize,
nat_alto: usize,
datos: &[u8],
) {
if nat_ancho == 0 || nat_alto == 0 {
return;
}
// Centrar el fotograma natural dentro del marco. Si el natural excede
// al marco, el desplazamiento queda en cero y el sobrante se recorta.
let off_x = marco.x + marco.ancho.saturating_sub(nat_ancho) / 2;
let off_y = marco.y + marco.alto.saturating_sub(nat_alto) / 2;
let marco_x_fin = marco.x + marco.ancho;
let marco_y_fin = marco.y + marco.alto;
for (indice, trozo) in datos.chunks_exact(4).enumerate() {
let columna = indice % region.ancho;
let fila = indice / region.ancho;
if fila >= region.alto {
break; // el fotograma excede el alto de la region: se ignora el resto
let columna = indice % nat_ancho;
let fila = indice / nat_ancho;
if fila >= nat_alto {
break; // el fotograma excede su alto natural: se ignora el resto
}
let x = off_x + columna;
let y = off_y + fila;
// Recorte firme: al marco —el confinamiento de la app— y al lienzo.
if x >= marco_x_fin || y >= marco_y_fin {
continue;
}
let x = region.x + columna;
let y = region.y + fila;
if x >= self.lienzo.ancho || y >= self.lienzo.alto {
continue; // recorte firme: nada se pinta fuera del lienzo
continue;
}
let p = u32::from_le_bytes([trozo[0], trozo[1], trozo[2], trozo[3]]);
let color = Color {
@@ -148,13 +169,32 @@ impl Consola {
}
/// Inunda una region entera con un color plano y la presenta. Es la baliza
/// de desalojo: cuando una aplicacion falla, su ventana se tatua de purpura.
/// de desalojo: cuando una aplicacion falla, su marco se tatua de purpura.
fn pintar_region(&mut self, region: RegionPantalla, color: Color) {
self.lienzo
.rellenar_rect(region.x, region.y, region.ancho, region.alto, color);
self.presentar();
}
/// 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
/// como una rejilla de paneles aun antes de que sus apps pinten nada.
fn pintar_escenario(&mut self, area: RegionPantalla, marcos: &[RegionPantalla]) {
self.lienzo.rellenar_rect(
area.x,
area.y,
area.ancho,
area.alto,
Color::LIENZO_EN_REPOSO,
);
for marco in marcos {
self.lienzo
.rellenar_rect(marco.x, marco.y, marco.ancho, marco.alto, Color::PANEL);
}
self.presentar();
}
/// Vuelca el lienzo sobre la pantalla fisica.
pub(crate) fn presentar(&mut self) {
self.pantalla.presentar(&self.lienzo);
@@ -167,11 +207,25 @@ 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— dentro de la
/// region de pantalla que el kernel asigno a esa aplicacion.
pub(crate) fn volcar_marco_wasm(region: RegionPantalla, datos: &[u8]) {
/// 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(
marco: RegionPantalla,
nat_ancho: usize,
nat_alto: usize,
datos: &[u8],
) {
if let Some(consola) = CONSOLA.get() {
consola.lock().volcar_marco(region, datos);
consola.lock().volcar_marco(marco, nat_ancho, nat_alto, datos);
}
}
/// 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.
pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) {
if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_escenario(area, marcos);
}
}
+9 -7
View File
@@ -43,6 +43,15 @@ impl Color {
b: 0x20,
};
/// Panel del compositor (Fase 8): un slate apenas mas claro que el reposo.
/// Tiñe cada marco teselado, de modo que el teselado se vea —como una
/// rejilla de paneles— aunque sus apps aun no hayan pintado nada.
pub(crate) const PANEL: Color = Color {
r: 0x1B,
g: 0x21,
b: 0x30,
};
/// Alerta de colapso: un rojo saturado, imposible de ignorar.
pub(crate) const ALERTA: Color = Color {
r: 0xD4,
@@ -101,13 +110,6 @@ pub(crate) struct RegionPantalla {
pub(crate) alto: usize,
}
impl RegionPantalla {
/// Numero total de pixeles que abarca la region.
pub(crate) const fn pixeles(&self) -> usize {
self.ancho * self.alto
}
}
/// Traduce un [`Color`] logico al valor nativo de 32 bits que el framebuffer
/// espera, respetando el orden de canales que reporta el firmware UEFI.
pub(crate) fn codificar(formato: PixelFormat, color: Color) -> u32 {
+47 -23
View File
@@ -19,6 +19,7 @@
// interrupcion: el bus PCI y el virtio-blk (Fases 6.1, 6.2).
// almacen — el grafo de objetos direccionado por contenido (Fase 6.1c).
// manifiesto — el Manifiesto de Genesis: que apps nacen del grafo (Fase 7).
// compositor — el teselado de las ventanas con `mirada-layout` (Fase 8).
// memory — el heap dinamico del kernel (`#[global_allocator]`).
// async_system — el reactor cooperativo: ejecutor, tareas, wakers, teclado
// y el reloj que marca el compas de los fotogramas (Fase 5).
@@ -46,6 +47,7 @@ use spin::Mutex;
mod almacen;
mod async_system;
mod baliza;
mod compositor;
mod consola;
mod drivers;
mod gdt;
@@ -66,7 +68,8 @@ use async_system::executor::Executor;
use baliza::BALIZA_PANICO;
use consola::{Consola, CONSOLA};
use grafico::{
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, ALTO_MAX, ANCHO_MAX,
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX,
ANCHO_MAX,
};
/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU.
@@ -99,7 +102,7 @@ async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
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.region(), falla.color_baliza());
consola::pintar_desalojo(app.marco(), falla.color_baliza());
return;
}
}
@@ -129,27 +132,42 @@ 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 su region y la
/// despacha como tarea cooperativa del reactor. Si el bytecode falta, esta
/// corrupto, o la carga fracasa, se salda pintando la region de la app 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) {
let region = manifiesto::region(entrada);
/// 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,
) {
// 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.
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
// —y la app se niega, no se instancia un modulo en el que no se confia.
let bytecode = match almacen::recuperar(&entrada.bytecode) {
Ok(Some(objeto)) => objeto.datos,
_ => {
consola::pintar_desalojo(region, Color::DESALOJO);
consola::pintar_desalojo(marco, 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`.
match wasm::AplicacionWasm::cargar(&bytecode, region, entrada.techo_memoria as usize, indice) {
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(region, Color::DESALOJO),
Err(_) => consola::pintar_desalojo(marco, Color::DESALOJO),
}
}
@@ -170,7 +188,7 @@ fn reportar(linea: &str) {
/// aplicacion. Toda falla se reporta a la consola y NO detiene el arranque: el
/// kernel se levanta con las apps que pueda — o con ninguna, si el grafo no
/// tiene userspace.
fn cargar_userspace(ejecutor: &mut Executor) {
fn cargar_userspace(ejecutor: &mut Executor, ancho_pantalla: usize, alto_pantalla: usize) {
let manifiesto = match manifiesto::cargar() {
Ok(Some(m)) => Some(m),
// Disco sin manifiesto anclado: `boot` no lo sembro. El kernel se
@@ -197,11 +215,20 @@ fn cargar_userspace(ejecutor: &mut Executor) {
if let Some(m) = manifiesto {
// Instalar el manifiesto VIVO ANTES de instanciar las apps: el `init`
// de cada app puede consultar su estado persistido (Fase 7c), y esa
// consulta lee del manifiesto vivo. Se instala una copia; la otra se
// itera para encender cada app con su indice — su identidad.
// consulta lee del manifiesto vivo.
manifiesto::instalar(m.clone());
for (indice, entrada) in m.apps.iter().enumerate() {
encender_app(ejecutor, indice, entrada);
// 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);
}
}
}
@@ -304,12 +331,9 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
Some(m) => m,
None => detener(),
};
let mut lienzo = Lienzo::nuevo(
memoria,
info.width.min(ANCHO_MAX),
info.height.min(ALTO_MAX),
formato,
);
let ancho_lienzo = info.width.min(ANCHO_MAX);
let alto_lienzo = info.height.min(ALTO_MAX);
let mut lienzo = Lienzo::nuevo(memoria, ancho_lienzo, alto_lienzo, formato);
lienzo.limpiar(Color::LIENZO_EN_REPOSO);
let mut consola = Consola::nueva(lienzo, pantalla);
@@ -347,7 +371,7 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
// compas de los fotogramas y la IRQ del teclado difundira cada
// scancode a los canales que las apps consultan. ---
let mut ejecutor = Executor::nuevo();
cargar_userspace(&mut ejecutor);
cargar_userspace(&mut ejecutor, ancho_lienzo, alto_lienzo);
// FASE 6.2 :: una tarea mas del reactor — no una app WASM— que sondea el
// disco de forma ASINCRONA: la demostracion de que la IRQ del disco
// conduce la E/S sin detener a las aplicaciones visuales.
+19 -10
View File
@@ -35,8 +35,14 @@ use crate::grafico::RegionPantalla;
/// 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 {
/// La sub-region de pantalla asignada a la aplicacion.
pub(crate) region: RegionPantalla,
/// 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`.
pub(crate) natural_ancho: usize,
pub(crate) natural_alto: usize,
/// El canal de teclado propio de la aplicacion.
pub(crate) canal: CanalTeclado,
/// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
@@ -92,15 +98,17 @@ pub(crate) fn enlazar_capacidades(
"renaser",
"sys_render_frame",
|caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> {
let region = caller.data().region;
let marco = caller.data().marco;
let nat_ancho = caller.data().natural_ancho;
let nat_alto = caller.data().natural_alto;
// El fotograma debe medir EXACTAMENTE los pixeles de la region. Un
// tamaño distinto delata a una app que pinta fuera de su ventana:
// El fotograma debe medir EXACTAMENTE el lienzo natural de la app.
// Un tamaño distinto delata a una app que pinta fuera de su lienzo:
// se aborta antes de tocar un byte.
let esperado = region.pixeles() * 4;
let esperado = nat_ancho * nat_alto * 4;
if len as usize != esperado {
return Err(Error::new(
"WASM :: sys_render_frame con un fotograma ajeno a la region asignada",
"WASM :: sys_render_frame con un fotograma ajeno al lienzo natural",
));
}
@@ -109,15 +117,16 @@ pub(crate) fn enlazar_capacidades(
// VALIDACION INFRANQUEABLE: si (ptr, len) se sale de la memoria
// lineal del modulo, se aborta la app —no el kernel—.
let marco = rango(
let fotograma = rango(
datos,
ptr,
len as usize,
"WASM :: sys_render_frame desbordo la memoria lineal del modulo",
)?;
// Limites verificados: la region es segura de leer y componer.
crate::volcar_marco_wasm(region, marco);
// 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);
Ok(())
},
)?;
+18 -12
View File
@@ -70,9 +70,9 @@ pub struct AplicacionWasm {
/// `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<(), ()>,
/// La region de pantalla de la app — su ventana, y donde se tatua su baliza
/// de desalojo si llega a fallar.
region: RegionPantalla,
/// 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 {
@@ -84,13 +84,16 @@ impl AplicacionWasm {
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
/// el reactor en cada pulso del reloj.
///
/// `techo_memoria` es la cuota de memoria lineal de ESTA app, en bytes —
/// desde la Fase 7 la dicta su `EntradaApp` del manifiesto. `indice_app` es
/// su posicion en el manifiesto: su identidad para las capacidades de
/// `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).
pub fn cargar(
bytecode: &[u8],
region: RegionPantalla,
marco: RegionPantalla,
natural_ancho: usize,
natural_alto: usize,
techo_memoria: usize,
indice_app: usize,
) -> Result<AplicacionWasm, FallaApp> {
@@ -119,7 +122,9 @@ impl AplicacionWasm {
let mut almacen = Store::new(
&motor,
ContextoCapacidades {
region,
marco,
natural_ancho,
natural_alto,
canal,
limites,
indice_app,
@@ -162,7 +167,7 @@ impl AplicacionWasm {
Ok(AplicacionWasm {
almacen,
func_tick,
region,
marco,
})
}
@@ -189,9 +194,10 @@ impl AplicacionWasm {
}
}
/// La region de pantalla asignada a la aplicacion.
pub fn region(&self) -> RegionPantalla {
self.region
/// 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
}
}