Files
brahman/renaser/kernel/src/consola.rs
T
sergio 65c88ccf25 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>
2026-05-22 18:56:40 +00:00

241 lines
9.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// renaser :: kernel/src/consola.rs — la superficie de texto e imagen
// -----------------------------------------------------------------------------
// La consola une el lienzo intermedio, la pantalla fisica y una pluma de
// escritura. Rasteriza cada glifo con `fontdue` al vuelo —el texto convertido,
// por fin, en dibujo— y tambien sabe volcar fotogramas crudos del userspace
// WASM. Es global y serializada tras un `Mutex`: las tareas escriben en ella.
// =============================================================================
use spin::{Mutex, Once};
use crate::grafico::{codificar, Color, Lienzo, Pantalla, RegionPantalla};
use crate::texto;
/// Margen del texto respecto al borde del lienzo, en pixeles.
const MARGEN: usize = 40;
/// Tamaño de la tipografia, en pixeles.
const TAM_FUENTE: f32 = 30.0;
/// Altura de avance de una linea de texto, en pixeles.
const ALTO_LINEA: usize = 40;
/// Posicion vertical de la primera linea base.
const BASE_INICIAL: usize = MARGEN + 30;
/// Interpola dos colores segun una cobertura: `0` => `fondo`, `255` => `tinta`.
fn mezclar(fondo: Color, tinta: Color, cobertura: u8) -> Color {
let canal = |a: u8, b: u8| -> u8 {
let c = cobertura as u16;
((a as u16 * (255 - c) + b as u16 * c) / 255) as u8
};
Color {
r: canal(fondo.r, tinta.r),
g: canal(fondo.g, tinta.g),
b: canal(fondo.b, tinta.b),
}
}
/// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
pub(crate) struct Consola {
lienzo: Lienzo,
pantalla: Pantalla,
/// Posicion horizontal de la pluma de escritura.
pluma_x: usize,
/// Linea base vertical de la pluma de escritura.
base_y: usize,
}
// SEGURIDAD: `Consola` encierra, via `Pantalla`, un puntero crudo al
// framebuffer. Ese puntero es valido durante toda la vida del kernel y todo
// acceso a la consola se serializa tras un `Mutex`. En un sistema de un solo
// nucleo, esto la hace segura de compartir entre el hilo principal y las tareas.
unsafe impl Send for Consola {}
impl Consola {
/// Crea una consola con la pluma en la esquina superior izquierda.
pub(crate) fn nueva(lienzo: Lienzo, pantalla: Pantalla) -> Consola {
Consola {
lienzo,
pantalla,
pluma_x: MARGEN,
base_y: BASE_INICIAL,
}
}
/// Lleva la pluma al inicio de la siguiente linea. Al llegar al fondo,
/// limpia el lienzo: una pizarra nueva.
fn nueva_linea(&mut self) {
self.pluma_x = MARGEN;
self.base_y += ALTO_LINEA;
if self.base_y + ALTO_LINEA >= self.lienzo.alto {
self.lienzo.limpiar(Color::LIENZO_EN_REPOSO);
self.base_y = BASE_INICIAL;
}
}
/// Escribe un caracter: rasteriza su glifo y avanza la pluma.
fn escribir_char(&mut self, caracter: char) {
if caracter == '\n' {
self.nueva_linea();
return;
}
let (metricas, cobertura) = texto::rasterizar(caracter, TAM_FUENTE);
// Salto de linea automatico al alcanzar el margen derecho.
if self.pluma_x + metricas.advance_width as usize + MARGEN > self.lienzo.ancho {
self.nueva_linea();
}
self.dibujar_glifo(&metricas, &cobertura);
self.pluma_x += metricas.advance_width as usize;
}
/// Escribe una cadena completa, caracter a caracter.
pub(crate) fn escribir(&mut self, texto: &str) {
for caracter in texto.chars() {
self.escribir_char(caracter);
}
}
/// Funde un mapa de cobertura de `fontdue` sobre el lienzo, en la pluma.
fn dibujar_glifo(&mut self, metricas: &fontdue::Metrics, cobertura: &[u8]) {
// Origen del glifo: la pluma desplazada por las metricas. El mapa de
// `fontdue` se recorre de arriba a abajo desde la cima del glifo.
let inicio_x = self.pluma_x as isize + metricas.xmin as isize;
let inicio_y = self.base_y as isize - metricas.ymin as isize - metricas.height as isize;
for fila in 0..metricas.height {
for col in 0..metricas.width {
let opacidad = cobertura[fila * metricas.width + col];
if opacidad == 0 {
continue; // pixel transparente: no toca el fondo
}
let x = inicio_x + col as isize;
let y = inicio_y + fila as isize;
if x < 0 || y < 0 {
continue;
}
let color = mezclar(Color::LIENZO_EN_REPOSO, Color::TEXTO, opacidad);
self.lienzo.pintar_pixel(x as usize, y as usize, color);
}
}
}
/// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
/// 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 % 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;
}
if x >= self.lienzo.ancho || y >= self.lienzo.alto {
continue;
}
let p = u32::from_le_bytes([trozo[0], trozo[1], trozo[2], trozo[3]]);
let color = Color {
r: (p >> 16) as u8,
g: (p >> 8) as u8,
b: p as u8,
};
self.lienzo.pixeles[y * self.lienzo.ancho + x] =
codificar(self.lienzo.formato, color);
}
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) {
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);
}
}
/// La consola global de renaser. Se funde en el arranque; despues, las tareas
/// 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(
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().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) {
if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_region(region, color);
}
}