65c88ccf25
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>
241 lines
9.6 KiB
Rust
241 lines
9.6 KiB
Rust
// =============================================================================
|
||
// 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);
|
||
}
|
||
}
|