⎇ dacfbad1249db14522037f8bcb6792560e1c3047
renaser/kernel/src/compositor.rs
Historia
Quién tocó qué
crudo
feat(renaser): Fase 8d — manipulación de ventanas
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 /// Promover la ventana enfocada a la posicion maestra del teselado.
59 Promover,
60 /// Mover la ventana enfocada una posicion adelante en el orden de teselado.
61 MoverAdelante,
62 /// Mover la ventana enfocada una posicion atras en el orden de teselado.
63 MoverAtras,
64}
65
66/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
67struct Ventana {
68 /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
69 natural_ancho: usize,
70 natural_alto: usize,
71 /// El marco teselado actual — donde la app vive en pantalla. Cambia con
72 /// cada re-teselado.
73 marco: RegionPantalla,
74 /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
75 /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
76 /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
77 /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
78 cache: Vec<u8>,
79 /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
80 /// solo ceros y no se recompone.
81 pintada: bool,
82 /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
83 /// vive; `Some(color)` la marca como muerta y la excluye del foco.
84 baliza: Option<Color>,
85}
86
87/// El escritorio: el registro de todas las ventanas y el modo de teselado.
88/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
89struct Escritorio {
90 modo: LayoutMode,
91 ancho: usize,
92 alto: usize,
93 /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
94 ventanas: Vec<Ventana>,
95 /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
96 /// ocupa esa celda del teselado. Una permutacion de `0..ventanas.len()`.
97 /// Separar el orden de la identidad permite promover y reordenar ventanas
98 /// sin tocar su `indice_app` —su canal de teclado, su ranura de estado—.
99 orden: Vec<usize>,
100}
101
102/// El escritorio global. Se funda una sola vez, en el arranque.
103static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
104
105/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
106/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
107/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
108static FOCO: AtomicUsize = AtomicUsize::new(0);
109
110/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
111/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
112/// compositor las drena desde el reactor cooperativo.
113static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
114
115// =============================================================================
116// Fundacion y consulta — el arranque
117// =============================================================================
118
119/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
120/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
121/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
122pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
123 MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
124
125 let mut ventanas = Vec::with_capacity(naturales.len());
126 for &(nat_ancho, nat_alto) in naturales {
127 ventanas.push(Ventana {
128 natural_ancho: nat_ancho,
129 natural_alto: nat_alto,
130 // Marco provisional; `aplicar_teselado` lo fija enseguida.
131 marco: RegionPantalla {
132 x: 0,
133 y: 0,
134 ancho: 0,
135 alto: 0,
136 },
137 // La cache: reservada UNA vez, acotada al lienzo natural.
138 cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
139 pintada: false,
140 baliza: None,
141 });
142 }
143
144 // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
145 // celda `i`. Los mandos del teclado lo permutaran.
146 let orden = (0..ventanas.len()).collect();
147 let mut escritorio = Escritorio {
148 modo: MODO_INICIAL,
149 ancho,
150 alto,
151 ventanas,
152 orden,
153 };
154 aplicar_teselado(&mut escritorio);
155
156 ESCRITORIO.call_once(|| Mutex::new(escritorio));
157}
158
159/// Recalcula el teselado y asigna a cada ventana su marco. La celda `slot` del
160/// teselado va a la ventana `orden[slot]`: manda el orden, no la identidad.
161fn aplicar_teselado(escritorio: &mut Escritorio) {
162 let marcos = teselar(
163 escritorio.orden.len(),
164 escritorio.ancho,
165 escritorio.alto,
166 escritorio.modo,
167 );
168 for (slot, marco) in marcos.into_iter().enumerate() {
169 let ventana = escritorio.orden[slot];
170 escritorio.ventanas[ventana].marco = marco;
171 }
172}
173
174/// Pinta el escenario inicial del compositor: el area de apps y sus marcos
175/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps.
176pub fn componer_escenario() {
177 let Some(escritorio) = ESCRITORIO.get() else {
178 return;
179 };
180 let escritorio = escritorio.lock();
181 let area = area_apps(escritorio.ancho, escritorio.alto);
182 let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
183 crate::consola::pintar_escenario(area, &marcos);
184}
185
186/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
187/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
188pub fn foco() -> usize {
189 FOCO.load(Ordering::Relaxed)
190}
191
192/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
193/// cola lock-free es seguro en contexto de interrupcion.
194pub fn solicitar(mando: Mando) {
195 if let Some(mandos) = MANDOS.get() {
196 // Si la cola se desborda, el mando se pierde en silencio: mas vale
197 // perder una pulsacion que arriesgar un panico dentro de una IRQ.
198 let _ = mandos.push(mando);
199 }
200}
201
202// =============================================================================
203// El fotograma de una app — cache y composicion
204// =============================================================================
205
206/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
207/// kernel asume la persistencia visual— y lo compone, centrado, en su marco.
208/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo.
209pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
210 let Some(escritorio) = ESCRITORIO.get() else {
211 return;
212 };
213 let (marco, nat_ancho, nat_alto) = {
214 let mut escritorio = escritorio.lock();
215 let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
216 return;
217 };
218 // Cachear el fotograma. El destino esta acotado al lienzo natural; se
219 // copia el minimo de ambas longitudes — jamas se desborda la cache.
220 let n = ventana.cache.len().min(datos.len());
221 ventana.cache[..n].copy_from_slice(&datos[..n]);
222 ventana.pintada = true;
223 (ventana.marco, ventana.natural_ancho, ventana.natural_alto)
224 };
225 let enfocada = FOCO.load(Ordering::Relaxed) == indice;
226 crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
227}
228
229/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
230/// Desde aqui queda excluida del foco — el teclado la salta.
231pub fn desalojar(indice: usize, color: Color) {
232 let Some(escritorio) = ESCRITORIO.get() else {
233 return;
234 };
235 let marco = {
236 let mut escritorio = escritorio.lock();
237 let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
238 return;
239 };
240 ventana.baliza = Some(color);
241 ventana.marco
242 };
243 let enfocada = FOCO.load(Ordering::Relaxed) == indice;
244 crate::consola::pintar_desalojo(marco, color, enfocada);
245}
246
247// =============================================================================
248// Los mandos del teclado — el escritorio interactivo
249// =============================================================================
250
251/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
252/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
253/// seguro bloquear el `ESCRITORIO` y la consola.
254pub fn atender_mandos() {
255 let Some(mandos) = MANDOS.get() else {
256 return;
257 };
258 while let Some(mando) = mandos.pop() {
259 match mando {
260 Mando::CiclarLayout => ciclar_layout(),
261 Mando::FocoSiguiente => mover_foco(true),
262 Mando::FocoAnterior => mover_foco(false),
263 Mando::Promover => promover(),
264 Mando::MoverAdelante => mover_ventana(true),
265 Mando::MoverAtras => mover_ventana(false),
266 }
267 }
268}
269
270/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las
271/// ventanas y recompone el escritorio entero desde las caches de respaldo.
272fn ciclar_layout() {
273 let Some(escritorio) = ESCRITORIO.get() else {
274 return;
275 };
276 let mut escritorio = escritorio.lock();
277 escritorio.modo = escritorio.modo.next();
278 aplicar_teselado(&mut escritorio);
279 redibujar_todo(&escritorio);
280}
281
282/// Mueve el foco a la siguiente ventana VIVA, recorriendo el ORDEN de teselado
283/// —de modo que el foco salte entre ventanas visualmente contiguas— y saltando
284/// las desalojadas. Redibuja la ventana que pierde el foco y la que lo gana.
285fn mover_foco(adelante: bool) {
286 let Some(escritorio) = ESCRITORIO.get() else {
287 return;
288 };
289 let escritorio = escritorio.lock();
290 let n = escritorio.orden.len();
291 if n == 0 {
292 return;
293 }
294 let anterior = FOCO.load(Ordering::Relaxed);
295 // La posicion del foco en el orden de teselado — el punto de partida.
296 let pos = escritorio
297 .orden
298 .iter()
299 .position(|&v| v == anterior)
300 .unwrap_or(0);
301
302 // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
303 // `n` pasos se vuelve al punto de partida y el foco no cambia.
304 let mut nueva_pos = pos;
305 let mut nuevo = anterior;
306 for _ in 0..n {
307 nueva_pos = if adelante {
308 (nueva_pos + 1) % n
309 } else {
310 (nueva_pos + n - 1) % n
311 };
312 let candidata = escritorio.orden[nueva_pos];
313 if escritorio.ventanas[candidata].baliza.is_none() {
314 nuevo = candidata;
315 break;
316 }
317 }
318 FOCO.store(nuevo, Ordering::Relaxed);
319
320 if let Some(ventana) = escritorio.ventanas.get(anterior) {
321 redibujar_ventana(ventana, false);
322 }
323 redibujar_ventana(&escritorio.ventanas[nuevo], true);
324}
325
326/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
327/// teselado: la saca de su lugar en el orden y la inserta al frente. Las demas
328/// se desplazan una posicion. El escritorio entero se recompone.
329fn promover() {
330 let Some(escritorio) = ESCRITORIO.get() else {
331 return;
332 };
333 let mut escritorio = escritorio.lock();
334 let foco = FOCO.load(Ordering::Relaxed);
335 if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
336 let ventana = escritorio.orden.remove(pos);
337 escritorio.orden.insert(0, ventana);
338 aplicar_teselado(&mut escritorio);
339 redibujar_todo(&escritorio);
340 }
341}
342
343/// Mueve la ventana enfocada una posicion en el orden de teselado,
344/// intercambiandola con su vecina. El foco viaja con la ventana — el indice no
345/// cambia, solo su lugar en el orden.
346fn mover_ventana(adelante: bool) {
347 let Some(escritorio) = ESCRITORIO.get() else {
348 return;
349 };
350 let mut escritorio = escritorio.lock();
351 let n = escritorio.orden.len();
352 if n < 2 {
353 return;
354 }
355 let foco = FOCO.load(Ordering::Relaxed);
356 if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
357 let destino = if adelante {
358 (pos + 1) % n
359 } else {
360 (pos + n - 1) % n
361 };
362 escritorio.orden.swap(pos, destino);
363 aplicar_teselado(&mut escritorio);
364 redibujar_todo(&escritorio);
365 }
366}
367
368/// Recompone el escritorio entero: repinta el escenario —area y paneles— con
369/// los marcos nuevos y, sobre el, cada ventana desde su cache de respaldo.
370fn redibujar_todo(escritorio: &Escritorio) {
371 let area = area_apps(escritorio.ancho, escritorio.alto);
372 let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
373 crate::consola::pintar_escenario(area, &marcos);
374
375 let foco = FOCO.load(Ordering::Relaxed);
376 for (i, ventana) in escritorio.ventanas.iter().enumerate() {
377 redibujar_ventana(ventana, i == foco);
378 }
379}
380
381/// Redibuja UNA ventana en su marco actual: si fue desalojada, su baliza; si ya
382/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel
383/// del escenario ya esta puesto—.
384fn redibujar_ventana(ventana: &Ventana, enfocada: bool) {
385 match ventana.baliza {
386 Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada),
387 None => {
388 if ventana.pintada {
389 crate::consola::volcar_marco(
390 ventana.marco,
391 ventana.natural_ancho,
392 ventana.natural_alto,
393 &ventana.cache,
394 enfocada,
395 );
396 }
397 }
398 }
399}
400
401// =============================================================================
402// Teselado — la geometria pura de `mirada-layout`
403// =============================================================================
404
405/// El area de pantalla que el compositor tesela: toda la pantalla menos la
406/// franja de la consola en la cima.
407pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
408 RegionPantalla {
409 x: 0,
410 y: FRANJA_CONSOLA.min(alto_pantalla),
411 ancho: ancho_pantalla,
412 alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
413 }
414}
415
416/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
417/// tiene exactamente `n` elementos, en el orden de las apps del manifiesto.
418fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
419 let area = area_apps(ancho, alto);
420 let pantalla = Rect::new(
421 area.x as i32,
422 area.y as i32,
423 area.ancho as i32,
424 area.alto as i32,
425 );
426 let params = LayoutParams {
427 mode: modo,
428 gap: MARGEN,
429 ..LayoutParams::default()
430 };
431 tile(pantalla, n, ¶ms)
432 .into_iter()
433 .map(rect_a_region)
434 .collect()
435}
436
437/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
438/// kernel (`usize`). Un rectangulo degenerado queda en cero.
439fn rect_a_region(r: Rect) -> RegionPantalla {
440 RegionPantalla {
441 x: r.x.max(0) as usize,
442 y: r.y.max(0) as usize,
443 ancho: r.w.max(0) as usize,
444 alto: r.h.max(0) as usize,
445 }
446}