8fc1d99ddf
renaser dialogaba sólo con el teclado; las ventanas flotantes nacían en cascada y allí se quedaban. La Fase 13 trae el ratón. - Driver `drivers/raton`: el ratón PS/2 cuelga del dispositivo auxiliar del 8042 + IRQ12. El driver despierta el aux, programa su IRQ, le ordena reportar, ensambla paquetes de 3 bytes con guarda del bit-3. Posición como atómicos, eventos como cola lock-free — el mismo guardarraíl que el teclado. - El puntero, capa de PRESENTACIÓN: `Pantalla::estampar_puntero` pinta un sprite de flecha 12×18 sobre el framebuffer después de copiar el lienzo. El lienzo nunca lo contiene — hace de save-under natural—. - Compositor: `atender_raton` drena eventos. Botón bajando es un clic-para-enfocar consistente con `mover_foco` (silencia bocina, alza si flota). Si la enfocada flota, arranca un arrastre con el desfase de agarre; el botón sostenido la sigue al puntero; al soltar, termina. - `refrescar_puntero` reestampa el framebuffer si el puntero se movió en una vuelta tranquila en que ninguna app pintó. Verificado en QEMU (mouse_move / mouse_button del monitor): el puntero aparece al arrancar, se mueve por la pantalla, un clic sobre pulso le da el foco, y un arrastre con el botón sostenido mueve la flotante de la cascada al centro-abajo. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
434 lines
15 KiB
Rust
434 lines
15 KiB
Rust
// =============================================================================
|
|
// renaser :: kernel/src/grafico.rs — el sustrato grafico del sistema
|
|
// -----------------------------------------------------------------------------
|
|
// En renaser el texto es un caso particular del dibujo, y el dibujo descansa
|
|
// sobre este modulo: el color, el framebuffer fisico (`Pantalla`) y el lienzo
|
|
// intermedio en RAM (`Lienzo`) que sostiene la tecnica de doble bufer.
|
|
// =============================================================================
|
|
|
|
use core::cell::UnsafeCell;
|
|
use core::ptr;
|
|
use core::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use bootloader_api::info::{FrameBuffer, FrameBufferInfo, PixelFormat};
|
|
use embedded_graphics::draw_target::DrawTarget;
|
|
use embedded_graphics::geometry::{OriginDimensions, Size};
|
|
use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
|
|
use embedded_graphics::Pixel;
|
|
|
|
/// Ancho maximo de lienzo soportado (Full HD).
|
|
pub(crate) const ANCHO_MAX: usize = 1920;
|
|
/// Alto maximo de lienzo soportado (Full HD).
|
|
pub(crate) const ALTO_MAX: usize = 1080;
|
|
/// Capacidad del lienzo intermedio, en pixeles de 32 bits.
|
|
const PIXELES_MAX: usize = ANCHO_MAX * ALTO_MAX;
|
|
|
|
// =============================================================================
|
|
// COLOR — la unidad indivisible del lenguaje visual de renaser
|
|
// =============================================================================
|
|
|
|
/// Color de 24 bits, independiente del formato fisico del framebuffer.
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct Color {
|
|
pub(crate) r: u8,
|
|
pub(crate) g: u8,
|
|
pub(crate) b: u8,
|
|
}
|
|
|
|
impl Color {
|
|
/// Reposo del lienzo: un indigo casi negro, sereno y persistente.
|
|
pub(crate) const LIENZO_EN_REPOSO: Color = Color {
|
|
r: 0x12,
|
|
g: 0x16,
|
|
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,
|
|
};
|
|
|
|
/// 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,
|
|
g: 0x1E,
|
|
b: 0x2C,
|
|
};
|
|
|
|
/// Agotamiento de memoria (OOM): un naranja de advertencia.
|
|
pub(crate) const OOM: Color = Color {
|
|
r: 0xFF,
|
|
g: 0xA5,
|
|
b: 0x00,
|
|
};
|
|
|
|
/// Tinta del texto: un blanco suave, legible sobre el indigo.
|
|
pub(crate) const TEXTO: Color = Color {
|
|
r: 0xE8,
|
|
g: 0xEC,
|
|
b: 0xF4,
|
|
};
|
|
|
|
/// Desalojo de una aplicacion: un purpura inequivoco. Distinto del rojo de
|
|
/// colapso del kernel y del naranja de OOM — porque esto NO es un colapso:
|
|
/// es el kernel conteniendo a un inquilino discolo y siguiendo vivo.
|
|
pub(crate) const DESALOJO: Color = Color {
|
|
r: 0x8B,
|
|
g: 0x5C,
|
|
b: 0xF6,
|
|
};
|
|
|
|
/// Desalojo por desbordo de memoria: un amarillo palido. Distingue al
|
|
/// inquilino que revento su techo ESPACIAL del que agoto su tiempo (purpura).
|
|
pub(crate) const DESALOJO_MEMORIA: Color = Color {
|
|
r: 0xFF,
|
|
g: 0xFF,
|
|
b: 0xE0,
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// REGION — la sub-superficie que el kernel asigna a cada aplicacion
|
|
// =============================================================================
|
|
|
|
/// Una sub-region rectangular de la pantalla, en pixeles. El kernel asigna una
|
|
/// a cada aplicacion del userspace: es, a la vez, su ventana al mundo y su
|
|
/// confinamiento — una app jamas pinta un pixel fuera de la suya.
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct RegionPantalla {
|
|
/// Desplazamiento horizontal de la esquina superior izquierda.
|
|
pub(crate) x: usize,
|
|
/// Desplazamiento vertical de la esquina superior izquierda.
|
|
pub(crate) y: usize,
|
|
/// Ancho de la region, en pixeles.
|
|
pub(crate) ancho: usize,
|
|
/// Alto de la region, en pixeles.
|
|
pub(crate) alto: usize,
|
|
}
|
|
|
|
/// 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 {
|
|
let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
|
|
match formato {
|
|
PixelFormat::Rgb => r | (g << 8) | (b << 16),
|
|
PixelFormat::Bgr => b | (g << 8) | (r << 16),
|
|
PixelFormat::U8 => (r * 54 + g * 183 + b * 19) >> 8,
|
|
PixelFormat::Unknown {
|
|
red_position,
|
|
green_position,
|
|
blue_position,
|
|
} => (r << red_position) | (g << green_position) | (b << blue_position),
|
|
_ => r | (g << 8) | (b << 16),
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ESCRITURA VOLATIL — la unica celula que toca memoria de video real
|
|
// =============================================================================
|
|
|
|
/// Deposita un pixel ya codificado en una direccion del framebuffer fisico.
|
|
/// Las escrituras son **volatiles**: el optimizador no puede elidirlas.
|
|
///
|
|
/// # Seguridad
|
|
///
|
|
/// `destino` debe apuntar a memoria de video valida y escribible para
|
|
/// `bytes_por_pixel` bytes, correctamente alineada para el ancho de escritura.
|
|
#[inline]
|
|
pub(crate) unsafe fn escribir_pixel_volatil(destino: *mut u8, valor: u32, bytes_por_pixel: usize) {
|
|
match bytes_por_pixel {
|
|
4 => unsafe { ptr::write_volatile(destino.cast::<u32>(), valor) },
|
|
3 => unsafe {
|
|
ptr::write_volatile(destino, valor as u8);
|
|
ptr::write_volatile(destino.add(1), (valor >> 8) as u8);
|
|
ptr::write_volatile(destino.add(2), (valor >> 16) as u8);
|
|
},
|
|
2 => unsafe { ptr::write_volatile(destino.cast::<u16>(), valor as u16) },
|
|
_ => unsafe { ptr::write_volatile(destino, valor as u8) },
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// LIENZO INTERMEDIO — el corazon del doble bufer
|
|
// =============================================================================
|
|
|
|
/// Respaldo estatico del lienzo, alineado a pagina. Vive en `.bss`.
|
|
#[repr(align(4096))]
|
|
struct LienzoEstatico(UnsafeCell<[u32; PIXELES_MAX]>);
|
|
|
|
// SEGURIDAD: el acceso se serializa mediante `LIENZO_ENTREGADO`, que garantiza
|
|
// un unico prestamo mutable durante toda la vida del sistema.
|
|
unsafe impl Sync for LienzoEstatico {}
|
|
|
|
/// Memoria de respaldo del lienzo intermedio.
|
|
static MEMORIA_LIENZO: LienzoEstatico = LienzoEstatico(UnsafeCell::new([0u32; PIXELES_MAX]));
|
|
|
|
/// Centinela de entrega unica del lienzo.
|
|
static LIENZO_ENTREGADO: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// Entrega — exactamente una vez — el prestamo mutable de la memoria del lienzo.
|
|
pub(crate) fn reclamar_memoria_lienzo() -> Option<&'static mut [u32]> {
|
|
if LIENZO_ENTREGADO.swap(true, Ordering::AcqRel) {
|
|
return None;
|
|
}
|
|
// SEGURIDAD: el `swap` anterior garantiza que este es el unico prestamo
|
|
// mutable de `MEMORIA_LIENZO` durante toda la ejecucion.
|
|
let arreglo: &'static mut [u32; PIXELES_MAX] = unsafe { &mut *MEMORIA_LIENZO.0.get() };
|
|
Some(arreglo.as_mut_slice())
|
|
}
|
|
|
|
/// Superficie de dibujo en RAM. Cada pixel se almacena ya codificado.
|
|
pub(crate) struct Lienzo {
|
|
pub(crate) pixeles: &'static mut [u32],
|
|
pub(crate) ancho: usize,
|
|
pub(crate) alto: usize,
|
|
pub(crate) formato: PixelFormat,
|
|
}
|
|
|
|
impl Lienzo {
|
|
/// Construye un lienzo sobre la memoria de respaldo reclamada.
|
|
pub(crate) fn nuevo(
|
|
memoria: &'static mut [u32],
|
|
ancho: usize,
|
|
alto: usize,
|
|
formato: PixelFormat,
|
|
) -> Lienzo {
|
|
Lienzo {
|
|
pixeles: memoria,
|
|
ancho,
|
|
alto,
|
|
formato,
|
|
}
|
|
}
|
|
|
|
/// Pinta un unico pixel. Las coordenadas fuera del lienzo se ignoran.
|
|
pub(crate) fn pintar_pixel(&mut self, x: usize, y: usize, color: Color) {
|
|
if x < self.ancho && y < self.alto {
|
|
self.pixeles[y * self.ancho + x] = codificar(self.formato, color);
|
|
}
|
|
}
|
|
|
|
/// Rellena un rectangulo, recortado con firmeza a los limites del lienzo.
|
|
pub(crate) fn rellenar_rect(
|
|
&mut self,
|
|
x0: usize,
|
|
y0: usize,
|
|
ancho: usize,
|
|
alto: usize,
|
|
color: Color,
|
|
) {
|
|
let valor = codificar(self.formato, color);
|
|
let x_ini = x0.min(self.ancho);
|
|
let y_ini = y0.min(self.alto);
|
|
let x_fin = (x0 + ancho).min(self.ancho);
|
|
let y_fin = (y0 + alto).min(self.alto);
|
|
|
|
let mut y = y_ini;
|
|
while y < y_fin {
|
|
let base = y * self.ancho;
|
|
let mut x = x_ini;
|
|
while x < x_fin {
|
|
self.pixeles[base + x] = valor;
|
|
x += 1;
|
|
}
|
|
y += 1;
|
|
}
|
|
}
|
|
|
|
/// Inunda el lienzo entero con un color plano.
|
|
pub(crate) fn limpiar(&mut self, color: Color) {
|
|
self.rellenar_rect(0, 0, self.ancho, self.alto, color);
|
|
}
|
|
}
|
|
|
|
// `embedded-graphics` ve el lienzo como un `DrawTarget`: sus primitivas
|
|
// vectoriales pueden dibujar directamente sobre el.
|
|
impl DrawTarget for Lienzo {
|
|
type Color = Rgb888;
|
|
type Error = core::convert::Infallible;
|
|
|
|
fn draw_iter<I>(&mut self, pixeles: I) -> Result<(), Self::Error>
|
|
where
|
|
I: IntoIterator<Item = Pixel<Self::Color>>,
|
|
{
|
|
for Pixel(punto, color) in pixeles {
|
|
if let (Ok(x), Ok(y)) = (usize::try_from(punto.x), usize::try_from(punto.y)) {
|
|
self.pintar_pixel(
|
|
x,
|
|
y,
|
|
Color {
|
|
r: color.r(),
|
|
g: color.g(),
|
|
b: color.b(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl OriginDimensions for Lienzo {
|
|
fn size(&self) -> Size {
|
|
Size::new(self.ancho as u32, self.alto as u32)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// PANTALLA — el framebuffer fisico GOP, envuelto en seguridad
|
|
// =============================================================================
|
|
|
|
/// Vista segura del framebuffer lineal entregado por el firmware UEFI.
|
|
pub(crate) struct Pantalla {
|
|
pub(crate) base: *mut u8,
|
|
pub(crate) ancho: usize,
|
|
pub(crate) alto: usize,
|
|
pub(crate) paso_bytes: usize,
|
|
pub(crate) bytes_por_pixel: usize,
|
|
/// Formato de pixel — necesario para estampar capas que se pintan
|
|
/// DIRECTAMENTE sobre el framebuffer, no sobre el lienzo (Fase 13).
|
|
pub(crate) formato: PixelFormat,
|
|
}
|
|
|
|
impl Pantalla {
|
|
/// Adopta el framebuffer descrito por `info`. La memoria de video es
|
|
/// permanente, asi que conservar su puntero crudo es legitimo.
|
|
pub(crate) fn adoptar(framebuffer: &mut FrameBuffer, info: FrameBufferInfo) -> Pantalla {
|
|
let base = framebuffer.buffer_mut().as_mut_ptr();
|
|
Pantalla {
|
|
base,
|
|
ancho: info.width,
|
|
alto: info.height,
|
|
paso_bytes: info.stride * info.bytes_per_pixel,
|
|
bytes_por_pixel: info.bytes_per_pixel,
|
|
formato: info.pixel_format,
|
|
}
|
|
}
|
|
|
|
/// Vuelca el lienzo intermedio sobre la pantalla fisica de un solo gesto.
|
|
pub(crate) fn presentar(&mut self, lienzo: &Lienzo) {
|
|
let ancho = self.ancho.min(lienzo.ancho);
|
|
let alto = self.alto.min(lienzo.alto);
|
|
|
|
for y in 0..alto {
|
|
let fila_fisica = y * self.paso_bytes;
|
|
let fila_lienzo = y * lienzo.ancho;
|
|
for x in 0..ancho {
|
|
let pixel = lienzo.pixeles[fila_lienzo + x];
|
|
// SEGURIDAD: x e y estan acotados por las dimensiones reales
|
|
// del framebuffer; el desplazamiento cae siempre dentro de el.
|
|
unsafe {
|
|
let destino = self.base.add(fila_fisica + x * self.bytes_por_pixel);
|
|
escribir_pixel_volatil(destino, pixel, self.bytes_por_pixel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// EL PUNTERO DEL RATON — un sprite estampado sobre el framebuffer (Fase 13)
|
|
// -----------------------------------------------------------------------------
|
|
// El puntero NO vive en el lienzo: el lienzo es el escritorio limpio, y se
|
|
// recompone con frecuencia. El puntero es una capa de PRESENTACION que cada
|
|
// volcado vuelve a sellar sobre el framebuffer, despues de copiar el lienzo.
|
|
// Asi no hay save-under que mantener: el lienzo HACE de save-under, y el
|
|
// framebuffer recibe el puntero como ultimo gesto.
|
|
// =============================================================================
|
|
|
|
/// Ancho del sprite del puntero, en pixeles.
|
|
const PUNTERO_ANCHO: usize = 12;
|
|
/// El sprite del puntero — una flecha noroeste. `#` es el borde oscuro, `*` el
|
|
/// relleno claro, `.` transparente.
|
|
const PUNTERO: [&[u8; PUNTERO_ANCHO]; 18] = [
|
|
b"#...........",
|
|
b"##..........",
|
|
b"#*#.........",
|
|
b"#**#........",
|
|
b"#***#.......",
|
|
b"#****#......",
|
|
b"#*****#.....",
|
|
b"#******#....",
|
|
b"#*******#...",
|
|
b"#********#..",
|
|
b"#*********#.",
|
|
b"#*****#####.",
|
|
b"#**#**#.....",
|
|
b"#*#.#**#....",
|
|
b"##..#**#....",
|
|
b"#....#**#...",
|
|
b".....#**#...",
|
|
b"......###...",
|
|
];
|
|
|
|
impl Pantalla {
|
|
/// Estampa el sprite del puntero del raton sobre el framebuffer, con su
|
|
/// vertice en (x, y). El sprite se recorta con firmeza a los limites de la
|
|
/// pantalla. NO altera el lienzo: la proxima recomposicion lo deja intacto;
|
|
/// el siguiente volcado lo vuelve a estampar (Fase 13).
|
|
pub(crate) fn estampar_puntero(&mut self, x: usize, y: usize) {
|
|
let borde = codificar(
|
|
self.formato,
|
|
Color {
|
|
r: 0x10,
|
|
g: 0x12,
|
|
b: 0x18,
|
|
},
|
|
);
|
|
let relleno = codificar(
|
|
self.formato,
|
|
Color {
|
|
r: 0xF0,
|
|
g: 0xF2,
|
|
b: 0xF8,
|
|
},
|
|
);
|
|
for (fila, linea) in PUNTERO.iter().enumerate() {
|
|
let py = y + fila;
|
|
if py >= self.alto {
|
|
break;
|
|
}
|
|
for (col, &celda) in linea.iter().enumerate() {
|
|
let px = x + col;
|
|
if px >= self.ancho {
|
|
continue;
|
|
}
|
|
let valor = match celda {
|
|
b'#' => borde,
|
|
b'*' => relleno,
|
|
_ => continue,
|
|
};
|
|
// SEGURIDAD: (px, py) acotado a las dimensiones reales del
|
|
// framebuffer; el desplazamiento cae dentro de la memoria de
|
|
// video que el firmware nos entrego.
|
|
unsafe {
|
|
let destino = self
|
|
.base
|
|
.add(py * self.paso_bytes + px * self.bytes_por_pixel);
|
|
escribir_pixel_volatil(destino, valor, self.bytes_por_pixel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|