tawasuyu

tawasuyu/brahman

Rust 321TOML 122JSON 14Markdown 114332 Definiciones

⎇ 5c462e6d307addd58614a8c4495f4becda04e4b2 renaser/kernel/src/compositor.rs Rust · 362 líneas · 14.0 KB Historia Quién tocó qué crudo

feat(renaser): Fases 8b y 8c — el escritorio interactivo 5c462e6d3 · sergio ·

1// =============================================================================
2//  renaser :: kernel/src/compositor.rs — Fase 8 :: el compositor teselante
3// -----------------------------------------------------------------------------
4//  El kernel no coloca las ventanas a mano: las TESELA. El motor es
5//  `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
6//  de brahman—, enlazado por `path` cruzando la frontera de workspace.
7//
8//  FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
9//  de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
10//  fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
11//  caliente —o mover el foco— y el kernel recompone cada ventana en su marco
12//  nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
13//  su imagen intacta a traves de cualquier reordenacion.
14//
15//  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
16//  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
17//  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
18//  un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
19//  estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
20//  pudiera disputar a una tarea cooperativa.
21// =============================================================================
22
23use core::sync::atomic::{AtomicUsize, Ordering};
24
25use alloc::vec;
26use alloc::vec::Vec;
27
28use crossbeam_queue::ArrayQueue;
29use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
30use spin::{Mutex, Once};
31
32use crate::grafico::{Color, RegionPantalla};
33
34/// Altura del strip superior reservado a la consola; las apps teselan debajo.
35/// La consola conserva ahi su registro de arranque completo —seis lineas,
36/// hasta la sonda asincrona de disco— legible sobre el teselado.
37const FRANJA_CONSOLA: usize = 296;
38
39/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
40const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
41
42/// Margen entre ventanas teseladas, en pixeles.
43const MARGEN: i32 = 14;
44
45/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
46const CAPACIDAD_MANDOS: usize = 32;
47
48/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
49/// atiende la tarea del compositor desde el reactor cooperativo.
50#[derive(Clone, Copy)]
51pub enum Mando {
52    /// Ciclar al siguiente modo de teselado de `mirada-layout`.
53    CiclarLayout,
54    /// Mover el foco a la siguiente ventana viva.
55    FocoSiguiente,
56    /// Mover el foco a la ventana viva anterior.
57    FocoAnterior,
58}
59
60/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
61struct Ventana {
62    /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
63    natural_ancho: usize,
64    natural_alto: usize,
65    /// El marco teselado actual — donde la app vive en pantalla. Cambia con
66    /// cada re-teselado.
67    marco: RegionPantalla,
68    /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
69    /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
70    /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
71    /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
72    cache: Vec<u8>,
73    /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
74    /// solo ceros y no se recompone.
75    pintada: bool,
76    /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
77    /// vive; `Some(color)` la marca como muerta y la excluye del foco.
78    baliza: Option<Color>,
79}
80
81/// El escritorio: el registro de todas las ventanas y el modo de teselado.
82/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
83struct Escritorio {
84    modo: LayoutMode,
85    ancho: usize,
86    alto: usize,
87    ventanas: Vec<Ventana>,
88}
89
90/// El escritorio global. Se funda una sola vez, en el arranque.
91static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
92
93/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
94/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
95/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
96static FOCO: AtomicUsize = AtomicUsize::new(0);
97
98/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
99/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
100/// compositor las drena desde el reactor cooperativo.
101static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
102
103// =============================================================================
104//  Fundacion y consulta — el arranque
105// =============================================================================
106
107/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
108/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
109/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
110pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
111    MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
112
113    let marcos = teselar(naturales.len(), ancho, alto, MODO_INICIAL);
114    let mut ventanas = Vec::with_capacity(naturales.len());
115    for (i, &(nat_ancho, nat_alto)) in naturales.iter().enumerate() {
116        ventanas.push(Ventana {
117            natural_ancho: nat_ancho,
118            natural_alto: nat_alto,
119            marco: marcos[i],
120            // La cache: reservada UNA vez, acotada al lienzo natural.
121            cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
122            pintada: false,
123            baliza: None,
124        });
125    }
126
127    ESCRITORIO.call_once(|| {
128        Mutex::new(Escritorio {
129            modo: MODO_INICIAL,
130            ancho,
131            alto,
132            ventanas,
133        })
134    });
135}
136
137/// Pinta el escenario inicial del compositor: el area de apps y sus marcos
138/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps.
139pub fn componer_escenario() {
140    let Some(escritorio) = ESCRITORIO.get() else {
141        return;
142    };
143    let escritorio = escritorio.lock();
144    let area = area_apps(escritorio.ancho, escritorio.alto);
145    let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
146    crate::consola::pintar_escenario(area, &marcos);
147}
148
149/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
150/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
151pub fn foco() -> usize {
152    FOCO.load(Ordering::Relaxed)
153}
154
155/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
156/// cola lock-free es seguro en contexto de interrupcion.
157pub fn solicitar(mando: Mando) {
158    if let Some(mandos) = MANDOS.get() {
159        // Si la cola se desborda, el mando se pierde en silencio: mas vale
160        // perder una pulsacion que arriesgar un panico dentro de una IRQ.
161        let _ = mandos.push(mando);
162    }
163}
164
165// =============================================================================
166//  El fotograma de una app — cache y composicion
167// =============================================================================
168
169/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
170/// kernel asume la persistencia visual— y lo compone, centrado, en su marco.
171/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo.
172pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
173    let Some(escritorio) = ESCRITORIO.get() else {
174        return;
175    };
176    let (marco, nat_ancho, nat_alto) = {
177        let mut escritorio = escritorio.lock();
178        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
179            return;
180        };
181        // Cachear el fotograma. El destino esta acotado al lienzo natural; se
182        // copia el minimo de ambas longitudes — jamas se desborda la cache.
183        let n = ventana.cache.len().min(datos.len());
184        ventana.cache[..n].copy_from_slice(&datos[..n]);
185        ventana.pintada = true;
186        (ventana.marco, ventana.natural_ancho, ventana.natural_alto)
187    };
188    let enfocada = FOCO.load(Ordering::Relaxed) == indice;
189    crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
190}
191
192/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
193/// Desde aqui queda excluida del foco — el teclado la salta.
194pub fn desalojar(indice: usize, color: Color) {
195    let Some(escritorio) = ESCRITORIO.get() else {
196        return;
197    };
198    let marco = {
199        let mut escritorio = escritorio.lock();
200        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
201            return;
202        };
203        ventana.baliza = Some(color);
204        ventana.marco
205    };
206    let enfocada = FOCO.load(Ordering::Relaxed) == indice;
207    crate::consola::pintar_desalojo(marco, color, enfocada);
208}
209
210// =============================================================================
211//  Los mandos del teclado — el escritorio interactivo
212// =============================================================================
213
214/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
215/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
216/// seguro bloquear el `ESCRITORIO` y la consola.
217pub fn atender_mandos() {
218    let Some(mandos) = MANDOS.get() else {
219        return;
220    };
221    while let Some(mando) = mandos.pop() {
222        match mando {
223            Mando::CiclarLayout => ciclar_layout(),
224            Mando::FocoSiguiente => mover_foco(true),
225            Mando::FocoAnterior => mover_foco(false),
226        }
227    }
228}
229
230/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las
231/// ventanas y recompone el escritorio entero desde las caches de respaldo.
232fn ciclar_layout() {
233    let Some(escritorio) = ESCRITORIO.get() else {
234        return;
235    };
236    let mut escritorio = escritorio.lock();
237    escritorio.modo = escritorio.modo.next();
238
239    let marcos = teselar(
240        escritorio.ventanas.len(),
241        escritorio.ancho,
242        escritorio.alto,
243        escritorio.modo,
244    );
245    for (ventana, marco) in escritorio.ventanas.iter_mut().zip(marcos) {
246        ventana.marco = marco;
247    }
248    redibujar_todo(&escritorio);
249}
250
251/// Mueve el foco a la siguiente ventana VIVA —saltando las desalojadas—; tras
252/// el salto, redibuja la ventana que pierde el foco y la que lo gana, para que
253/// el borde de cada una cambie de color.
254fn mover_foco(adelante: bool) {
255    let Some(escritorio) = ESCRITORIO.get() else {
256        return;
257    };
258    let escritorio = escritorio.lock();
259    let n = escritorio.ventanas.len();
260    if n == 0 {
261        return;
262    }
263    let anterior = FOCO.load(Ordering::Relaxed).min(n - 1);
264
265    // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
266    // `n` pasos se vuelve al punto de partida y el foco no cambia.
267    let mut nuevo = anterior;
268    for _ in 0..n {
269        nuevo = if adelante {
270            (nuevo + 1) % n
271        } else {
272            (nuevo + n - 1) % n
273        };
274        if escritorio.ventanas[nuevo].baliza.is_none() {
275            break;
276        }
277    }
278    FOCO.store(nuevo, Ordering::Relaxed);
279
280    redibujar_ventana(&escritorio.ventanas[anterior], false);
281    redibujar_ventana(&escritorio.ventanas[nuevo], true);
282}
283
284/// Recompone el escritorio entero: repinta el escenario —area y paneles— con
285/// los marcos nuevos y, sobre el, cada ventana desde su cache de respaldo.
286fn redibujar_todo(escritorio: &Escritorio) {
287    let area = area_apps(escritorio.ancho, escritorio.alto);
288    let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
289    crate::consola::pintar_escenario(area, &marcos);
290
291    let foco = FOCO.load(Ordering::Relaxed);
292    for (i, ventana) in escritorio.ventanas.iter().enumerate() {
293        redibujar_ventana(ventana, i == foco);
294    }
295}
296
297/// Redibuja UNA ventana en su marco actual: si fue desalojada, su baliza; si ya
298/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel
299/// del escenario ya esta puesto—.
300fn redibujar_ventana(ventana: &Ventana, enfocada: bool) {
301    match ventana.baliza {
302        Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada),
303        None => {
304            if ventana.pintada {
305                crate::consola::volcar_marco(
306                    ventana.marco,
307                    ventana.natural_ancho,
308                    ventana.natural_alto,
309                    &ventana.cache,
310                    enfocada,
311                );
312            }
313        }
314    }
315}
316
317// =============================================================================
318//  Teselado — la geometria pura de `mirada-layout`
319// =============================================================================
320
321/// El area de pantalla que el compositor tesela: toda la pantalla menos la
322/// franja de la consola en la cima.
323pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
324    RegionPantalla {
325        x: 0,
326        y: FRANJA_CONSOLA.min(alto_pantalla),
327        ancho: ancho_pantalla,
328        alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
329    }
330}
331
332/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
333/// tiene exactamente `n` elementos, en el orden de las apps del manifiesto.
334fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
335    let area = area_apps(ancho, alto);
336    let pantalla = Rect::new(
337        area.x as i32,
338        area.y as i32,
339        area.ancho as i32,
340        area.alto as i32,
341    );
342    let params = LayoutParams {
343        mode: modo,
344        gap: MARGEN,
345        ..LayoutParams::default()
346    };
347    tile(pantalla, n, &params)
348        .into_iter()
349        .map(rect_a_region)
350        .collect()
351}
352
353/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
354/// kernel (`usize`). Un rectangulo degenerado queda en cero.
355fn rect_a_region(r: Rect) -> RegionPantalla {
356    RegionPantalla {
357        x: r.x.max(0) as usize,
358        y: r.y.max(0) as usize,
359        ancho: r.w.max(0) as usize,
360        alto: r.h.max(0) as usize,
361    }
362}