gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

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>

65c88ccf250831dad70bbffc3d27ec96d80f4274 · ruwaq sergio · (5 killa ñawpaq) · tayta 8fc26b0c0 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 23

renaser/kernel/src/compositor.rs

renaser/kernel/src/consola.rs

renaser/kernel/src/grafico.rs

renaser/kernel/src/main.rs

renaser/kernel/src/wasm/env.rs

renaser/kernel/src/wasm/mod.rs

11 qillqakuna · +393 −77

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index ed24566..245bbc2 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -630,3 +630,45 @@ separado, a un reinicio.
   - **Reinicio** — `boot` respeta el disco; memoriosa despierta con las 5
     celdas intactas y el testigo en ámbar: su `init` releyó el estado del
     grafo. El estado por-app sobrevivió al apagón.
+
+## Fase 8a — El compositor teselante — 2026-05-22
+
+Hasta la Fase 7, cada app llevaba su región escrita a mano en el manifiesto —
+coordenadas fijas, una composición rígida. La Fase 8 entrega esa decisión a un
+COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA con
+`mirada-layout`.
+
+### Añadido
+- **`kernel/compositor.rs`** — el compositor teselante. Enlaza `mirada-layout`
+  —el mismo núcleo `no_std` que ordena las ventanas del compositor Wayland de
+  brahman, ya enlazado por `path` cruzando la frontera de workspace— y, con su
+  algoritmo `MasterStack`, calcula un marco para cada app. `disponer(n, …)`
+  devuelve un marco por ventana; `area_apps`, la zona teselable (la pantalla
+  menos la franja superior de la consola).
+- `consola`: `pintar_escenario` —inunda el área de apps y tiñe cada marco con
+  el color de panel, de modo que el teselado se vea como una rejilla— y el
+  color `Color::PANEL`.
+
+### Cambiado
+- `consola::volcar_marco` ya no deposita el fotograma en `(region.x, region.y)`:
+  CENTRA el fotograma natural de la app dentro de su marco teselado y lo
+  recorta a sus bordes.
+- `ContextoCapacidades` lleva ahora `marco` (el rectángulo teselado) y
+  `natural_ancho`/`natural_alto` (el tamaño del lienzo de la app);
+  `sys_render_frame` valida el fotograma contra el tamaño natural.
+- `AplicacionWasm` / `encender_app` / `cargar_userspace` cablean el marco
+  teselado. `cargar_userspace` recibe las dimensiones de pantalla, tesela con
+  el compositor y pinta el escenario antes de encender las apps.
+- `RegionPantalla::pixeles` se retira — sin uso desde que `sys_render_frame`
+  mide contra el tamaño natural.
+- **Las apps NO cambian.** Renderizan su lienzo natural, fijo; el kernel lo
+  compone centrado en el marco. El compositor reordena la pantalla sin que
+  ninguna app toque una sola instrucción.
+
+### Verificado
+- QEMU (captura headless): las cinco apps de génesis aparecen teseladas en
+  `MasterStack` — `hola` como ventana maestra (izquierda, 60% del ancho) y
+  memoriosa, discola, glotona y cronista apiladas a la derecha. Cada lienzo,
+  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.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index b84c152..1f80ce4 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -45,10 +45,10 @@ cp target/wasm32-unknown-unknown/release/<app>.wasm ../../kernel/assets/<app>.wa
 - `apps/` — aplicaciones del userspace, módulos `.wasm`
   (target `wasm32-unknown-unknown`). Workspaces propios, excluidos.
 
-Módulos del kernel (`kernel/src/`): `main`, `grafico`, `consola`, `baliza`,
-`sync`, `gdt`, `interrupts`, `pic`, `drivers/`, `almacen`, `memory/`,
-`async_system/`, `texto`, `wasm/`. El detalle de cada uno está en
-`ARCHITECTURE.md`.
+Módulos del kernel (`kernel/src/`): `main`, `grafico`, `consola`, `compositor`,
+`baliza`, `sync`, `gdt`, `interrupts`, `pic`, `drivers/`, `almacen`,
+`manifiesto`, `memory/`, `async_system/`, `texto`, `wasm/`. El detalle de cada
+uno está en `ARCHITECTURE.md`.
 
 ## Toolchain
 
@@ -75,8 +75,9 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
 
 Fases 1 a 5, 6.0, 6.1, 6.2 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)—. Todo verificado en QEMU. Ver
-`ROADMAP.md`.
+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`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index 0365bf9..8168f83 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -318,6 +318,30 @@ Sus inquilinos cierran los ojos en el apagón y los abren, al volver, justo
 donde los cerraron. La casa ya no sólo perdura: recuerda, uno por uno, a los
 suyos.
 
+## El plano prestado
+
+Hasta hoy, cada inquilino tenía su habitación marcada con coordenadas exactas
+en el cuaderno: aquí empieza tu cuarto, aquí acaba. Funcionaba, pero era
+rígido — mover un tabique exigía reescribir el cuaderno a mano.
+
+Hoy la casa contrató a un arquitecto. Ya nadie dibuja las habitaciones a mano:
+se le dice cuántos inquilinos hay y él reparte el suelo entero —una pieza
+amplia para el principal, las demás en una columna ordenada al lado—, sin
+huecos ni solapes, ajustando cada cuarto al número de quienes viven. A eso se
+le llama teselar, y es un oficio delicado.
+
+Lo más hermoso es de dónde salió ese arquitecto. renaser tiene una casa
+hermana —mirada, el escritorio que vive sobre Linux— y esa casa ya tenía uno,
+con buen ojo para repartir cuartos. En vez de contratar a otro, renaser le
+pidió prestado el suyo: la misma cabeza, el mismo plano, sirviendo a dos
+mundos tan distintos como un escritorio gráfico y un núcleo desnudo sobre el
+metal. Una sola sabiduría, dos casas.
+
+Y los inquilinos ni se enteraron. Cada uno sigue pintando su cuadro del tamaño
+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.
+
 ---
 
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/FASE8.md b/renaser/FASE8.md
new file mode 100644
index 0000000..0e4ed24
--- /dev/null
+++ b/renaser/FASE8.md
@@ -0,0 +1,56 @@
+# renaser — Fase 8 :: El compositor teselante
+
+Plan de ataque. Para el estado general ver `ROADMAP.md`; para la arquitectura,
+`ARCHITECTURE.md`.
+
+## El objetivo
+
+Hasta la Fase 7, cada aplicación llevaba su región escrita a mano en el
+Manifiesto de Génesis: cuatro coordenadas fijas (`region_x`, `region_y`,
+`region_ancho`, `region_alto`). El kernel las leía y componía cada app en su
+rectángulo, inmóvil — una disposición rígida, decidida en la siembra.
+
+La Fase 8 entrega esa decisión a un COMPOSITOR. El kernel deja de colocar las
+ventanas a mano: las **tesela**. El motor del teselado es `mirada-layout` — el
+mismo núcleo `no_std` que ordena las ventanas del compositor Wayland de
+brahman, enlazado por `path` cruzando la frontera de workspace. Una sola lógica
+de teselado sirve a dos mundos: el escritorio sobre Linux y el kernel
+bare-metal.
+
+## Sub-fases
+
+### 8a — El compositor tesela — ✅ HECHA
+
+- **`compositor.rs`** — enlaza `mirada-layout` y calcula un marco por app con
+  el algoritmo `MasterStack`, dentro del área de pantalla (todo menos la franja
+  superior reservada a la consola).
+- El kernel **centra** el fotograma natural de cada app dentro de su marco
+  teselado; las apps conservan su tamaño natural y no cambian una instrucción.
+- `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
+
+- 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.
+
+### 8c — Foco y `Workspace`
+
+- 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.
+
+## Estructura de archivos
+
+| archivo | estado | rol en la Fase 8 |
+| --- | --- | --- |
+| `kernel/src/compositor.rs` | **nuevo (8a)** | teselado con `mirada-layout` |
+| `kernel/src/consola.rs` | a modificar (8a) | composición centrada en el marco |
+| `kernel/src/wasm/mod.rs` | a modificar (8a) | `marco` + tamaño natural |
+| `kernel/src/wasm/env.rs` | a modificar (8a) | validación contra el natural |
+| `kernel/src/main.rs` | a modificar (8a) | el compositor en el arranque |
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index 07f3248..bb95b60 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -120,8 +120,24 @@ 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.
 
-Líneas abiertas posteriores: más capacidades del host (temporización, audio);
-la Fase 8 — el compositor sobre `mirada-layout`.
+## Fase 8 — el compositor teselante
+
+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
+brahman—, enlazado por `path` cruzando la frontera de workspace. Plan completo
+en `FASE8.md`.
+
+- **8a — el compositor tesela (completada).** `compositor.rs` calcula un marco
+  por app con el algoritmo `MasterStack`. El kernel centra el fotograma natural
+  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`.
+
+Líneas abiertas posteriores: más capacidades del host (temporización, audio).
 
 ## Principios que persisten entre fases
 
diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs
new file mode 100644
index 0000000..ac51508
--- /dev/null
+++ b/renaser/kernel/src/compositor.rs
@@ -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,
+    }
+}
diff --git a/renaser/kernel/src/consola.rs b/renaser/kernel/src/consola.rs
index 68796a6..8ed5886 100644
--- a/renaser/kernel/src/consola.rs
+++ b/renaser/kernel/src/consola.rs
@@ -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(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().volcar_marco(region, datos);
+        consola.lock().pintar_escenario(area, marcos);
     }
 }
 
diff --git a/renaser/kernel/src/grafico.rs b/renaser/kernel/src/grafico.rs
index 721d12b..2edecb5 100644
--- a/renaser/kernel/src/grafico.rs
+++ b/renaser/kernel/src/grafico.rs
@@ -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 {
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index 4a0ecaa..4f63a8b 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -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.
diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs
index 1b7a3d7..b560cec 100644
--- a/renaser/kernel/src/wasm/env.rs
+++ b/renaser/kernel/src/wasm/env.rs
@@ -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(())
         },
     )?;
diff --git a/renaser/kernel/src/wasm/mod.rs b/renaser/kernel/src/wasm/mod.rs
index d908cef..b6c3bf6 100644
--- a/renaser/kernel/src/wasm/mod.rs
+++ b/renaser/kernel/src/wasm/mod.rs
@@ -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
     }
 }