⎇ 5c462e6d307addd58614a8c4495f4becda04e4b2
renaser/kernel/src/compositor.rs
Historia
Quién tocó qué
crudo
feat(renaser): Fases 8b y 8c — el escritorio interactivo
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, ¶ms)
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}