gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

feat(renaser): Fase 13 — ratón, puntero y arrastre de flotantes

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>

8fc1d99ddff618eaa0c2c29f4a51eaaed73443dd · por sergio · (hace 5 meses) · padre d1b700eb2 · ver el árbol en este commit

Delta semántico · 49

renaser/kernel/src/compositor.rs

renaser/kernel/src/consola.rs

renaser/kernel/src/drivers/mod.rs

renaser/kernel/src/drivers/raton.rs

renaser/kernel/src/grafico.rs

renaser/kernel/src/interrupts.rs

renaser/kernel/src/main.rs

11 archivos · +644 −4

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index 6f603c1..9a5d1f8 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -906,3 +906,43 @@ capacidades. Hasta hoy renaser sólo sabía DIBUJAR para llamar la atención.
   pcspk-audiodev`), el PCM capturado —50 s— es una onda cuadrada oscilante de
   ±24 000 de amplitud, con una frecuencia media de ~375 Hz: la de la escala de
   Do mayor.
+
+## Fase 13 — Ratón, puntero y arrastre de flotantes — 2026-05-22
+
+renaser tenía teclado y bocina como entrada/salida del usuario, pero no
+puntero. Las ventanas flotantes de la Fase 9 nacían en cascada y se quedaban
+ahí, clavadas. La Fase 13 trae el ratón: un puntero en pantalla, clic-para-
+enfocar y arrastre del marco de las ventanas flotantes.
+
+### Añadido
+- **Driver `drivers/raton`** — el ratón PS/2 cuelga del dispositivo auxiliar
+  del 8042 (la misma controladora que el teclado) y anuncia cada movimiento
+  por la IRQ12. El driver despierta el aux, enciende su IRQ, le ordena
+  reportar, y ensambla los paquetes de 3 bytes con la guarda del bit-3 que
+  detecta desincronización. Como el teclado, la IRQ12 sólo toca atómicos
+  —posición del puntero— y una cola lock-free de eventos.
+- **El puntero, capa de presentación.** `Pantalla` gana `formato` y la
+  función `estampar_puntero`: un sprite de flecha de 12×18 que se pinta
+  DIRECTAMENTE sobre el framebuffer, justo después de copiar el lienzo. El
+  lienzo permanece libre de puntero —hace de save-under natural—; cada
+  `presentar` sella el puntero al final.
+- **Capacidad del compositor**: `atender_raton` drena eventos del ratón cada
+  fotograma. El botón izquierdo bajando es un CLIC: enfoca la ventana viva
+  bajo el puntero (consistente con `mover_foco` — silencia la bocina, alza al
+  frente si flota). Si la enfocada es flotante, arranca un ARRASTRE con el
+  desfase de agarre; con el botón sostenido, la ventana sigue al puntero;
+  soltarlo lo termina.
+- **`refrescar_puntero`**: en una vuelta tranquila en que ninguna app pinte,
+  reestampa el framebuffer si el puntero se ha movido — el centinela
+  empacado evita repintar dos veces el mismo instante.
+- `Escritorio` gana `arrastre: Option<Arrastre>` y `raton_izq: bool`.
+  `Arrastre` guarda el índice de la ventana asida y el desfase de agarre.
+  `cerrar` libera el arrastre si la ventana cerrada era la arrastrada.
+
+### Verificado
+- QEMU (`mouse_move` + `mouse_button` del monitor). El puntero aparece en el
+  centro al arrancar; `mouse_move` lo lleva por la pantalla. Clic sobre la
+  ventana `pulso` (en la pila) la enfoca: el borde índigo deja la maestra y
+  envuelve a `pulso`. `Alt+F` la flota; el ratón la agarra y la arrastra de
+  la esquina superior izquierda al centro-abajo de la pantalla con el botón
+  sostenido. El kernel sigue estable a través de los gestos.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index 1031367..8ed7150 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -80,9 +80,10 @@ del teclado (8c), promoción y reordenación de ventanas (8d)—, la Fase 9
 COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—
 la Fase 10 COMPLETA —alta y baja de aplicaciones en vivo (`Alt+N` / `Alt+Q`)—,
 la Fase 11 COMPLETA —el reloj del sistema como capacidad de host
-(`sys_tiempo_mono`) + la app `pulso`— y la Fase 12 COMPLETA —la bocina del PC
-como capacidad de host (`sys_tono`) + la app `tonada`—. Todo verificado en
-QEMU. Ver `ROADMAP.md`.
+(`sys_tiempo_mono`) + la app `pulso`—, la Fase 12 COMPLETA —la bocina del PC
+como capacidad de host (`sys_tono`) + la app `tonada`— y la Fase 13 COMPLETA
+—ratón PS/2, puntero, clic-para-enfocar y arrastre de ventanas flotantes—.
+Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index c20af8f..55661ef 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -476,6 +476,26 @@ escala, una y otra vez, y la dibuja como una escalera de luces que sube al
 compás de la música. Míralo cantar en silencio desde cualquier rincón;
 acércate —dale el foco— y lo oirás.
 
+## El dedo — la casa aprende a señalar
+
+La casa sabía escuchar y hablar; sabía mirar y dibujar. Lo que le faltaba era
+señalar. Sus habitantes vivían en sus cuartos sin que pudiéramos tocarlos —si
+queríamos cambiar de cuarto, había que cantarle a la casa qué tecla tocar—.
+Era una casa de palabras, sin gestos.
+
+Hoy le crece un dedo. Una flecha pequeña, blanca con su borde oscuro, que
+aparece en mitad del salón cuando la casa abre los ojos. Se mueve por las
+paredes y los cuartos siguiendo lo que la mano de afuera quiera. Y allí donde
+señala, ahí está la atención: tocar un cuarto es elegirlo —el borde de la
+mirada se traslada al que apuntas, sin más palabras—.
+
+Y los cuartos que flotaban, que hasta ayer nacían en su cascada y allí se
+quedaban, hoy pueden moverse de sitio. Se les agarra por donde uno los toca,
+con el dedo apretado, y se les lleva por la casa como si llevara uno una
+maceta de un balcón al otro. La mano suelta, el cuarto se queda donde lo dejó.
+Por fin la disposición no la dicta sólo la casa: también la conversación entre
+quien vive dentro y quien habita fuera.
+
 ---
 
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index 6c86b82..f352c68 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -211,6 +211,23 @@ se suma a la matriz de capacidades. Verificada en QEMU.
 - Verificado por captura: la onda cuadrada de la bocina, enrutada a un WAV,
   late a la frecuencia media de la escala.
 
+## Fase 13 — ratón, puntero y arrastre de flotantes (completada)
+
+Hasta la Fase 12 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: un puntero,
+clic-para-enfocar y arrastre del marco de las flotantes. Verificada en QEMU
+(`mouse_move` / `mouse_button` del monitor).
+
+- Driver `drivers/raton`: el ratón PS/2 cuelga del dispositivo auxiliar del
+  8042 + la IRQ12. El driver despierta el aux, programa su IRQ, le ordena
+  reportar, ensambla paquetes de 3 bytes con guarda del bit-3.
+- El puntero como capa de presentación: `Pantalla::estampar_puntero` pinta un
+  sprite de flecha de 12×18 sobre el framebuffer, DESPUÉS de copiar el
+  lienzo. El lienzo nunca lo contiene — hace de save-under natural—.
+- El compositor gana `atender_raton`: botón bajando → clic-para-enfocar (sobre
+  cualquier ventana viva); si la enfocada flota, arranca un ARRASTRE con el
+  desfase de agarre; el botón sostenido la sigue al puntero; al soltar, fin.
+
 Líneas abiertas posteriores: reciclado de las ranuras de ventana cerradas;
 audio con varias voces (PCM) más allá del tono único de la bocina.
 
diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs
index 03e542a..d8d4240 100644
--- a/renaser/kernel/src/compositor.rs
+++ b/renaser/kernel/src/compositor.rs
@@ -29,6 +29,14 @@
 //  ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
 //  ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
 //
+//  FASE 13 :: el raton entra en juego. Hay un PUNTERO en pantalla y el
+//  compositor gana dos gestos: clic-para-enfocar (sobre cualquier ventana viva)
+//  y ARRASTRAR una flotante con el boton izquierdo sostenido. Como el teclado
+//  y la bocina, los eventos del raton vienen del manejador de IRQ12 por una
+//  cola lock-free; `atender_raton` los drena cooperativamente y, al detectar
+//  un boton que baja o un arrastre en curso, mueve el foco o el marco. Los
+//  cuartos flotantes dejan, por fin, de estar clavados en su cascada.
+//
 //  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
 //  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
 //  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
@@ -96,6 +104,16 @@ pub enum Mando {
     Lanzar,
 }
 
+/// Un arrastre EN CURSO (Fase 13): el indice de la ventana flotante asida con
+/// el raton y el desfase con que se asio —para que la ventana no salte al
+/// agarrarla, sino que siga al puntero como si lo llevara cogido por ahi—.
+#[derive(Clone, Copy)]
+struct Arrastre {
+    ventana: usize,
+    agarre_dx: usize,
+    agarre_dy: usize,
+}
+
 /// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
 struct Ventana {
     /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
@@ -139,6 +157,11 @@ struct Escritorio {
     /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
     /// de `0..ventanas.len()`.
     flotantes: Vec<usize>,
+    /// ¿Estaba el boton izquierdo del raton pulsado en el evento anterior?
+    /// Para detectar las transiciones —el momento exacto del clic o de soltar—.
+    raton_izq: bool,
+    /// Arrastre en curso, si lo hay (Fase 13).
+    arrastre: Option<Arrastre>,
 }
 
 /// El escritorio global. Se funda una sola vez, en el arranque.
@@ -201,6 +224,8 @@ pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
         ventanas,
         orden,
         flotantes: Vec::new(),
+        raton_izq: false,
+        arrastre: None,
     };
     aplicar_teselado(&mut escritorio);
 
@@ -589,6 +614,10 @@ fn cerrar() {
     // indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
     escritorio.orden.retain(|&v| v != foco);
     escritorio.flotantes.retain(|&v| v != foco);
+    // Si la estabamos arrastrando con el raton (Fase 13), soltarla.
+    if escritorio.arrastre.map(|a| a.ventana) == Some(foco) {
+        escritorio.arrastre = None;
+    }
     // El foco salta a la primera ventana viva que quede; si no queda ninguna,
     // se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
     let nuevo = escritorio
@@ -661,6 +690,146 @@ pub fn partos_pendientes() -> usize {
     PARTOS.swap(0, Ordering::Relaxed)
 }
 
+// =============================================================================
+//  FASE 13 — raton, puntero y arrastre de ventanas flotantes
+// =============================================================================
+
+/// La ultima posicion del puntero que el compositor REFRESCO. Si la posicion
+/// actual del raton coincide con esta, no hay nada nuevo que estampar; si
+/// difiere, la consola debe volver a presentar. Empacada como `y * 65536 + x`,
+/// con `usize::MAX` como centinela de «aun no he visto al raton».
+static PUNTERO_REFRESCADO: AtomicUsize = AtomicUsize::new(usize::MAX);
+
+/// Drena los eventos del raton y los aplica: clic enfoca la ventana bajo el
+/// puntero (y, si flota, inicia un arrastre); el boton sostenido la arrastra;
+/// soltarlo termina el gesto. La invoca la tarea del compositor en cada
+/// fotograma, desde el reactor cooperativo.
+pub fn atender_raton() {
+    let Some(escritorio) = ESCRITORIO.get() else {
+        return;
+    };
+    let mut escritorio = escritorio.lock();
+    let mut cambio = false;
+    while let Some(evento) = crate::drivers::raton::siguiente_evento() {
+        let izq = evento.botones & 0b001 != 0;
+        let x = evento.x as usize;
+        let y = evento.y as usize;
+        let izq_antes = escritorio.raton_izq;
+        if izq && !izq_antes {
+            // Boton bajó: un CLIC sobre el punto (x, y).
+            if let Some(v) = ventana_en(&escritorio, x, y) {
+                let viva = {
+                    let w = &escritorio.ventanas[v];
+                    w.baliza.is_none() && !w.cerrada
+                };
+                if viva {
+                    // Enfocar como hace `mover_foco`: foco + bocina muda + alza
+                    // si flota.
+                    FOCO.store(v, Ordering::Relaxed);
+                    crate::drivers::altavoz::tono(0);
+                    alzar_si_flota(&mut escritorio, v);
+                    // Si la ventana flota, empezar a arrastrarla.
+                    if escritorio.flotantes.contains(&v) {
+                        let marco = escritorio.ventanas[v].marco;
+                        escritorio.arrastre = Some(Arrastre {
+                            ventana: v,
+                            agarre_dx: x.saturating_sub(marco.x),
+                            agarre_dy: y.saturating_sub(marco.y),
+                        });
+                    }
+                    cambio = true;
+                }
+            }
+        } else if izq && izq_antes {
+            // Boton sostenido: arrastrar la ventana asida, si la hay.
+            if let Some(arr) = escritorio.arrastre {
+                mover_arrastrada(&mut escritorio, arr, x, y);
+                cambio = true;
+            }
+        } else if !izq && izq_antes {
+            // Boton subió: fin del arrastre.
+            escritorio.arrastre = None;
+        }
+        escritorio.raton_izq = izq;
+    }
+    if cambio {
+        recomponer(&escritorio);
+        // El recomponer ya presento; sincronizar el centinela para no presentar
+        // dos veces en la misma vuelta.
+        PUNTERO_REFRESCADO.store(empacar_puntero(), Ordering::Relaxed);
+    }
+}
+
+/// La ventana topmost que contiene el punto (x, y), si la hay. Recorre el
+/// orden-Z de delante hacia atras: primero las flotantes (la ultima es la
+/// frontal), despues las teseladas.
+fn ventana_en(escritorio: &Escritorio, x: usize, y: usize) -> Option<usize> {
+    for &v in escritorio.flotantes.iter().rev() {
+        if contiene(escritorio.ventanas[v].marco, x, y) {
+            return Some(v);
+        }
+    }
+    for &v in escritorio.orden.iter().rev() {
+        if contiene(escritorio.ventanas[v].marco, x, y) {
+            return Some(v);
+        }
+    }
+    None
+}
+
+/// ¿Contiene el marco al punto (x, y)?
+fn contiene(marco: RegionPantalla, x: usize, y: usize) -> bool {
+    x >= marco.x && x < marco.x + marco.ancho && y >= marco.y && y < marco.y + marco.alto
+}
+
+/// Mueve la ventana arrastrada de modo que el punto del puntero —la asa— siga
+/// estando, dentro de la ventana, donde se asio. La ventana queda acotada al
+/// area de apps.
+fn mover_arrastrada(escritorio: &mut Escritorio, arr: Arrastre, x: usize, y: usize) {
+    let area = area_apps(escritorio.ancho, escritorio.alto);
+    let Some(ventana) = escritorio.ventanas.get_mut(arr.ventana) else {
+        return;
+    };
+    let ancho = ventana.marco.ancho;
+    let alto = ventana.marco.alto;
+    let mut nx = x.saturating_sub(arr.agarre_dx);
+    let mut ny = y.saturating_sub(arr.agarre_dy);
+    // Acotar al area de apps: la ventana entera ha de caber dentro.
+    if nx + ancho > area.x + area.ancho {
+        nx = (area.x + area.ancho).saturating_sub(ancho);
+    }
+    if ny + alto > area.y + area.alto {
+        ny = (area.y + area.alto).saturating_sub(alto);
+    }
+    nx = nx.max(area.x);
+    ny = ny.max(area.y);
+    ventana.marco.x = nx;
+    ventana.marco.y = ny;
+}
+
+/// Empaca la posicion actual del puntero en un solo `usize` —`y * 65536 + x`—
+/// para compararla atomicamente con la ultima refrescada. `usize::MAX` indica
+/// «el raton no esta vivo».
+fn empacar_puntero() -> usize {
+    match crate::drivers::raton::posicion() {
+        Some((x, y)) => (y << 16) | (x & 0xFFFF),
+        None => usize::MAX,
+    }
+}
+
+/// Si el puntero se ha movido desde la ultima presentacion del compositor, le
+/// pide a la consola un volcado fresco —para reestampar el puntero en su
+/// nuevo lugar—. La invoca la tarea del compositor cada fotograma.
+pub fn refrescar_puntero() {
+    let actual = empacar_puntero();
+    if actual == usize::MAX {
+        return;
+    }
+    if PUNTERO_REFRESCADO.swap(actual, Ordering::Relaxed) != actual {
+        crate::consola::refrescar();
+    }
+}
+
 // =============================================================================
 //  Teselado — la geometria pura de `mirada-layout`
 // =============================================================================
diff --git a/renaser/kernel/src/consola.rs b/renaser/kernel/src/consola.rs
index 6850ea1..8fa6800 100644
--- a/renaser/kernel/src/consola.rs
+++ b/renaser/kernel/src/consola.rs
@@ -291,9 +291,15 @@ impl Consola {
         );
     }
 
-    /// Vuelca el lienzo sobre la pantalla fisica.
+    /// Vuelca el lienzo sobre la pantalla fisica y estampa el puntero del raton
+    /// como ultima capa, directamente sobre el framebuffer (Fase 13). El lienzo
+    /// permanece libre de puntero — es el «save-under» natural—; el framebuffer
+    /// lo recibe en cada volcado, asi que el puntero esta siempre encima.
     pub(crate) fn presentar(&mut self) {
         self.pantalla.presentar(&self.lienzo);
+        if let Some((x, y)) = crate::drivers::raton::posicion() {
+            self.pantalla.estampar_puntero(x, y);
+        }
     }
 }
 
@@ -338,3 +344,12 @@ pub(crate) fn pintar_desalojo(marco: RegionPantalla, color: Color, enfocada: boo
         consola.lock().pintar_region(marco, color, enfocada);
     }
 }
+
+/// Vuelve a volcar el lienzo a pantalla y estampar el puntero (Fase 13). Sirve
+/// para refrescar el puntero cuando el raton se mueve pero ninguna app pinta:
+/// el lienzo es el mismo, pero el puntero esta en otro sitio.
+pub(crate) fn refrescar() {
+    if let Some(consola) = CONSOLA.get() {
+        consola.lock().presentar();
+    }
+}
diff --git a/renaser/kernel/src/drivers/mod.rs b/renaser/kernel/src/drivers/mod.rs
index 80a0135..25a8258 100644
--- a/renaser/kernel/src/drivers/mod.rs
+++ b/renaser/kernel/src/drivers/mod.rs
@@ -11,8 +11,11 @@
 //                  lectura, por sondeo, de su primer sector.
 //    * `altavoz` — la bocina del PC: el canal 2 del PIT como generador de tono
 //                  (Fase 12).
+//    * `raton`   — el raton PS/2: el dispositivo auxiliar del 8042 + IRQ12,
+//                  paquetes de 3 bytes (Fase 13).
 // =============================================================================
 
 pub mod altavoz;
 pub mod disco;
 pub mod pci;
+pub mod raton;
diff --git a/renaser/kernel/src/drivers/raton.rs b/renaser/kernel/src/drivers/raton.rs
new file mode 100644
index 0000000..ceb65c7
--- /dev/null
+++ b/renaser/kernel/src/drivers/raton.rs
@@ -0,0 +1,261 @@
+// =============================================================================
+//  renaser :: kernel/src/drivers/raton.rs — Fase 13 :: el raton PS/2
+// -----------------------------------------------------------------------------
+//  El raton cuelga del controlador 8042 —el mismo que sirve el teclado—, por su
+//  puerto AUXILIAR, y anuncia cada movimiento por la IRQ12. Su lenguaje es un
+//  paquete de tres bytes: banderas (botones + signos), delta X y delta Y.
+//
+//  Como el teclado en la Fase 8c, el raton respeta el GUARDARRAIL de
+//  interrupciones: el manejador de IRQ12 solo toca atomicos —la posicion del
+//  puntero— y una cola lock-free de eventos. Jamas un cerrojo cooperativo. El
+//  compositor drena esa cola desde el reactor, a su ritmo.
+// =============================================================================
+
+use core::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
+
+use crossbeam_queue::ArrayQueue;
+use spin::Once;
+use x86_64::instructions::port::Port;
+
+use crate::pic;
+
+/// Puerto de datos del controlador 8042 (compartido con el teclado).
+const DATOS: u16 = 0x60;
+/// Puerto de estado / comando del 8042.
+const ESTADO: u16 = 0x64;
+
+/// Capacidad de la cola de eventos del raton — holgada: nadie agita tanto.
+const CAPACIDAD: usize = 128;
+
+/// Un evento del raton, tal como lo entrega un paquete completo: la posicion
+/// ya absoluta del puntero y el estado crudo de los botones (bit 0 izquierdo,
+/// bit 1 derecho, bit 2 central). Lo produce la IRQ12; lo consume el compositor.
+#[derive(Clone, Copy)]
+pub struct EventoRaton {
+    pub x: u16,
+    pub y: u16,
+    pub botones: u8,
+}
+
+/// Posicion del puntero, en pixeles. La escribe la IRQ12 a cada paquete; la lee
+/// la consola para estampar el puntero, y el compositor para situar los clics.
+static RATON_X: AtomicUsize = AtomicUsize::new(0);
+static RATON_Y: AtomicUsize = AtomicUsize::new(0);
+
+/// Limites de la pantalla — el puntero jamas se sale de ellos.
+static ANCHO: AtomicUsize = AtomicUsize::new(0);
+static ALTO: AtomicUsize = AtomicUsize::new(0);
+
+/// ¿Esta el raton inicializado y vivo? Hasta entonces no hay puntero que pintar.
+static ACTIVO: AtomicBool = AtomicBool::new(false);
+
+/// Estado del ensamblado del paquete de 3 bytes. Lo toca SOLO la IRQ12, que es
+/// no-reentrante en un solo nucleo: una secuencia de atomicos basta, sin cerrojo.
+static FASE: AtomicUsize = AtomicUsize::new(0);
+static BYTE0: AtomicU8 = AtomicU8::new(0);
+static BYTE1: AtomicU8 = AtomicU8::new(0);
+/// Estado de los botones en el paquete anterior — para detectar transiciones.
+static BOTONES_ANTES: AtomicU8 = AtomicU8::new(0);
+
+/// La cola de eventos: la IRQ12 deposita (lock-free, segura en interrupcion),
+/// el compositor drena desde el reactor cooperativo.
+static EVENTOS: Once<ArrayQueue<EventoRaton>> = Once::new();
+
+// =============================================================================
+//  Dialogo con el 8042 — esperas acotadas, sin colgar jamas el arranque
+// =============================================================================
+
+/// Espera, con un tope de intentos, a que el 8042 admita una escritura (su
+/// bufer de entrada vacio). Devuelve `false` si se agota la paciencia.
+fn esperar_envio() -> bool {
+    for _ in 0..100_000 {
+        // SEGURIDAD: 0x64 es el puerto de estado del 8042, fijo en el PC.
+        let estado = unsafe { Port::<u8>::new(ESTADO).read() };
+        if estado & 0b10 == 0 {
+            return true;
+        }
+    }
+    false
+}
+
+/// Espera, con un tope de intentos, a que el 8042 tenga un byte que entregar.
+fn esperar_recepcion() -> bool {
+    for _ in 0..100_000 {
+        // SEGURIDAD: ver `esperar_envio`.
+        let estado = unsafe { Port::<u8>::new(ESTADO).read() };
+        if estado & 0b01 != 0 {
+            return true;
+        }
+    }
+    false
+}
+
+/// Escribe un byte en el puerto de comando del 8042.
+fn comando_8042(byte: u8) {
+    esperar_envio();
+    // SEGURIDAD: 0x64 es el puerto de comando del 8042 en la arquitectura PC.
+    unsafe { Port::<u8>::new(ESTADO).write(byte) };
+}
+
+/// Escribe un byte en el puerto de datos del 8042.
+fn escribir_datos(byte: u8) {
+    esperar_envio();
+    // SEGURIDAD: 0x60 es el puerto de datos del 8042 en la arquitectura PC.
+    unsafe { Port::<u8>::new(DATOS).write(byte) };
+}
+
+/// Lee un byte del puerto de datos del 8042.
+fn leer_datos() -> u8 {
+    esperar_recepcion();
+    // SEGURIDAD: ver `escribir_datos`.
+    unsafe { Port::<u8>::new(DATOS).read() }
+}
+
+/// Envia un comando AL raton (no al 8042): el prefijo 0xD4 le dice al 8042 que
+/// el proximo byte de datos va al dispositivo auxiliar. Consume el ACK (0xFA).
+fn comando_raton(byte: u8) {
+    comando_8042(0xD4);
+    escribir_datos(byte);
+    let _ack = leer_datos();
+}
+
+/// Vacia el bufer de salida del 8042 — descarta bytes rezagados que pudieran
+/// desincronizar el ensamblado del primer paquete.
+fn vaciar() {
+    for _ in 0..16 {
+        // SEGURIDAD: ver `esperar_envio`.
+        let estado = unsafe { Port::<u8>::new(ESTADO).read() };
+        if estado & 0b01 == 0 {
+            return;
+        }
+        let _ = unsafe { Port::<u8>::new(DATOS).read() };
+    }
+}
+
+// =============================================================================
+//  Arranque
+// =============================================================================
+
+/// Funda el raton: despierta el dispositivo auxiliar del 8042, programa su IRQ,
+/// le ordena reportar movimiento y deja el puntero en el centro de la pantalla.
+/// Requiere el heap activo; debe invocarse una vez, antes de habilitar las
+/// interrupciones.
+pub fn init(ancho: usize, alto: usize) {
+    ANCHO.store(ancho, Ordering::Relaxed);
+    ALTO.store(alto, Ordering::Relaxed);
+    RATON_X.store(ancho / 2, Ordering::Relaxed);
+    RATON_Y.store(alto / 2, Ordering::Relaxed);
+    EVENTOS.call_once(|| ArrayQueue::new(CAPACIDAD));
+
+    vaciar();
+
+    // Despertar el dispositivo auxiliar (el raton) en el 8042.
+    comando_8042(0xA8);
+
+    // Leer el byte de configuracion, encender la IRQ del auxiliar (bit 1) y
+    // asegurar que su reloj corre (bit 5 a cero), y reescribirlo.
+    comando_8042(0x20);
+    let mut config = leer_datos();
+    config |= 0b0000_0010;
+    config &= !0b0010_0000;
+    comando_8042(0x60);
+    escribir_datos(config);
+
+    // Al raton: valores por defecto y, despues, reportar movimiento.
+    comando_raton(0xF6);
+    comando_raton(0xF4);
+
+    vaciar();
+    ACTIVO.store(true, Ordering::Relaxed);
+    // Levantar la mascara de la IRQ12 — el raton vive en el PIC esclavo.
+    pic::desenmascarar(12);
+}
+
+// =============================================================================
+//  El paquete de 3 bytes — punto de entrada desde la IRQ12
+// =============================================================================
+
+/// Punto de entrada DESDE el manejador de IRQ12. Ensambla el paquete de tres
+/// bytes y, al completarlo, actualiza la posicion del puntero y encola un
+/// evento. Deliberadamente breve y libre de panicos: corre en contexto de IRQ.
+pub fn recibir_byte(byte: u8) {
+    match FASE.load(Ordering::Relaxed) {
+        0 => {
+            // El primer byte SIEMPRE trae el bit 3 a 1. Si no, el flujo esta
+            // desincronizado: se descarta el byte y se sigue esperando uno bueno.
+            if byte & 0b0000_1000 == 0 {
+                return;
+            }
+            BYTE0.store(byte, Ordering::Relaxed);
+            FASE.store(1, Ordering::Relaxed);
+        }
+        1 => {
+            BYTE1.store(byte, Ordering::Relaxed);
+            FASE.store(2, Ordering::Relaxed);
+        }
+        _ => {
+            FASE.store(0, Ordering::Relaxed);
+            procesar(BYTE0.load(Ordering::Relaxed), BYTE1.load(Ordering::Relaxed), byte);
+        }
+    }
+}
+
+/// Procesa un paquete completo: traduce los deltas a una posicion absoluta del
+/// puntero, acotada a la pantalla, y encola el evento si hay algo que el
+/// compositor deba atender —un boton pulsado o un arrastre en curso—.
+fn procesar(banderas: u8, dx_crudo: u8, dy_crudo: u8) {
+    // Un paquete con desbordamiento de delta trae un salto disparatado: se
+    // ignora su movimiento por completo.
+    if banderas & 0b1100_0000 != 0 {
+        return;
+    }
+    let dx = dx_crudo as i8 as i32;
+    // El raton PS/2 da Y positivo hacia ARRIBA; la pantalla, hacia ABAJO.
+    let dy = dy_crudo as i8 as i32;
+
+    let ancho = ANCHO.load(Ordering::Relaxed) as i32;
+    let alto = ALTO.load(Ordering::Relaxed) as i32;
+    let x = (RATON_X.load(Ordering::Relaxed) as i32 + dx).clamp(0, (ancho - 1).max(0)) as usize;
+    let y = (RATON_Y.load(Ordering::Relaxed) as i32 - dy).clamp(0, (alto - 1).max(0)) as usize;
+    RATON_X.store(x, Ordering::Relaxed);
+    RATON_Y.store(y, Ordering::Relaxed);
+
+    // Encolar un evento SOLO si importa: si los botones cambiaron, o si alguno
+    // sigue pulsado —un arrastre—. El movimiento ocioso no satura la cola; el
+    // puntero, aun asi, ya se movio (los atomicos de arriba).
+    let botones = banderas & 0b0000_0111;
+    let antes = BOTONES_ANTES.swap(botones, Ordering::Relaxed);
+    if botones != antes || botones != 0 {
+        if let Some(cola) = EVENTOS.get() {
+            // Si la cola se desborda, el evento se pierde en silencio: mas vale
+            // perder un gesto que arriesgar un panico dentro de una IRQ.
+            let _ = cola.push(EventoRaton {
+                x: x as u16,
+                y: y as u16,
+                botones,
+            });
+        }
+    }
+}
+
+// =============================================================================
+//  Consulta — para la consola (puntero) y el compositor (eventos)
+// =============================================================================
+
+/// La posicion actual del puntero, o `None` si el raton aun no esta vivo. La
+/// consulta la consola para estampar el puntero en cada volcado de pantalla.
+pub fn posicion() -> Option<(usize, usize)> {
+    if !ACTIVO.load(Ordering::Relaxed) {
+        return None;
+    }
+    Some((
+        RATON_X.load(Ordering::Relaxed),
+        RATON_Y.load(Ordering::Relaxed),
+    ))
+}
+
+/// Extrae el siguiente evento del raton, o `None` si no hay ninguno pendiente.
+/// La drena el compositor, evento a evento, desde el reactor cooperativo.
+pub fn siguiente_evento() -> Option<EventoRaton> {
+    EVENTOS.get().and_then(ArrayQueue::pop)
+}
diff --git a/renaser/kernel/src/grafico.rs b/renaser/kernel/src/grafico.rs
index e04e07c..4417f46 100644
--- a/renaser/kernel/src/grafico.rs
+++ b/renaser/kernel/src/grafico.rs
@@ -305,6 +305,9 @@ pub(crate) struct Pantalla {
     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 {
@@ -318,6 +321,7 @@ impl Pantalla {
             alto: info.height,
             paso_bytes: info.stride * info.bytes_per_pixel,
             bytes_por_pixel: info.bytes_per_pixel,
+            formato: info.pixel_format,
         }
     }
 
@@ -341,3 +345,89 @@ impl Pantalla {
         }
     }
 }
+
+// =============================================================================
+//  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);
+                }
+            }
+        }
+    }
+}
diff --git a/renaser/kernel/src/interrupts.rs b/renaser/kernel/src/interrupts.rs
index 9d5e46a..47f436b 100644
--- a/renaser/kernel/src/interrupts.rs
+++ b/renaser/kernel/src/interrupts.rs
@@ -54,6 +54,7 @@ pub fn init() {
     // --- Interrupciones de hardware, ya remapeadas por el PIC ---
     idt[pic::VECTOR_TEMPORIZADOR].set_handler_fn(irq_temporizador);
     idt[pic::VECTOR_TECLADO].set_handler_fn(irq_teclado);
+    idt[pic::vector_irq(12)].set_handler_fn(irq_raton);
 
     let idt_estatica: &'static InterruptDescriptorTable = idt;
     idt_estatica.load();
@@ -136,6 +137,18 @@ extern "x86-interrupt" fn irq_teclado(_marco: InterruptStackFrame) {
     pic::fin_de_interrupcion(pic::VECTOR_TECLADO);
 }
 
+/// IRQ12 — Raton PS/2 (Fase 13). Lee un byte del puerto de datos del 8042 —el
+/// mismo que sirve al teclado—; el ensamblador de paquetes del raton se ocupa
+/// de juntar tres bytes y entregar el evento al compositor.
+extern "x86-interrupt" fn irq_raton(_marco: InterruptStackFrame) {
+    // SEGURIDAD: 0x60 es el puerto de datos del 8042 en la arquitectura PC.
+    // Que la IRQ12 ha disparado garantiza que el byte es del raton, no del
+    // teclado: el controlador alza una linea distinta para cada dispositivo.
+    let byte: u8 = unsafe { x86_64::instructions::port::Port::new(0x60).read() };
+    crate::drivers::raton::recibir_byte(byte);
+    pic::fin_de_interrupcion(pic::vector_irq(12));
+}
+
 /// IRQ del disco — virtio-blk (Fase 6.2). El disco ha terminado una
 /// transferencia: `atender_irq` reconoce la interrupcion en el dispositivo
 /// —lo que libera su linea— y despierta a la tarea que aguardaba el bloque.
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index 90c0336..eb85942 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -143,6 +143,11 @@ async fn tarea_compositor() {
     loop {
         async_system::reloj::EsperaFrame::nueva().await;
         compositor::atender_mandos();
+        // FASE 13 :: atender los eventos del raton (clic-para-enfocar y
+        // arrastre de flotantes), y refrescar el puntero si se movio en una
+        // vuelta tranquila en que ninguna app pinto.
+        compositor::atender_raton();
+        compositor::refrescar_puntero();
         // FASE 10 :: atender las altas en vivo. Por cada `Alt+N` pendiente,
         // dar a luz una aplicacion nueva — el compositor solo conto la
         // peticion; instanciar el WASM es trabajo del orquestador.
@@ -455,6 +460,12 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
         }
     }
 
+    // --- 6.6. FASE 13 :: despertar el raton PS/2. El 8042 enciende su
+    //          dispositivo auxiliar, el raton empieza a reportar, y el PIC
+    //          desenmascara su IRQ12. Desde aqui hay un puntero en pantalla,
+    //          y los clics pueden alcanzar al compositor.
+    drivers::raton::init(ancho_lienzo, alto_lienzo);
+
     // --- 7. FASE 7 :: levantar el reactor y poblar el userspace DESDE EL
     //        GRAFO. El kernel ya no empotra los modulos WASM: lee el
     //        Manifiesto de Genesis que `boot` sembro en la imagen de disco e