tawasuyu

tawasuyu/brahman

Rust 321TOML 122JSON 14Markdown 114332 Sutichaykuna

⎇ 922ad1f86bbef5d4f8c5f59827e5fd6a9b17c1af renaser/kernel/src/compositor.rs Rust · 709 siq'ikuna · 28.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(renaser): Fase 12 — la bocina del PC como capacidad de host 922ad1f86 · sergio ·

1// =============================================================================
2//  renaser :: kernel/src/compositor.rs — el compositor: teselado y flotantes
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//  FASE 9 :: orden-Z y ventanas flotantes. Una ventana puede ABANDONAR el
16//  teselado y FLOTAR —un marco propio, libre, que SOLAPA a las demas—. El
17//  escritorio separa entonces dos capas: las TESELADAS, al fondo, sin
18//  solapamiento entre si; y las FLOTANTES, encima, apiladas por un orden-Z
19//  —`flotantes` ES esa pila, de atras hacia adelante; la ultima es la frontal—.
20//  Con flotantes vivas el kernel deja de pintar cada ventana por separado:
21//  RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
22//  resuelva por el orden del pintado, sin recortes ni mascaras.
23//
24//  FASE 10 :: el escritorio deja de ser un censo fijo. Una ventana puede
25//  CERRARSE en vivo (`Alt+Q`): se la marca, su app concluye su tarea por su
26//  voluntad y el teselado reclama su espacio. Y puede NACER una ventana nueva
27//  (`Alt+N`): `nacer_ventana` la añade al censo y devuelve su indice al
28//  orquestador, que instancia su WASM y engendra su tarea. El censo de
29//  ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
30//  ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
31//
32//  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
33//  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
34//  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
35//  un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
36//  estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
37//  pudiera disputar a una tarea cooperativa.
38// =============================================================================
39
40use core::sync::atomic::{AtomicUsize, Ordering};
41
42use alloc::vec;
43use alloc::vec::Vec;
44
45use crossbeam_queue::ArrayQueue;
46use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
47use spin::{Mutex, Once};
48
49use crate::consola::{self, Capa, Contenido};
50use crate::grafico::{Color, RegionPantalla};
51
52/// Altura del strip superior reservado a la consola; las apps teselan debajo.
53/// La consola conserva ahi su registro de arranque completo —seis lineas,
54/// hasta la sonda asincrona de disco— legible sobre el teselado.
55const FRANJA_CONSOLA: usize = 296;
56
57/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
58const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
59
60/// Margen entre ventanas teseladas, en pixeles.
61const MARGEN: i32 = 14;
62
63/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
64const CAPACIDAD_MANDOS: usize = 32;
65
66/// Reborde de cromo de una ventana flotante: el panel que rodea su lienzo
67/// natural, donde se asienta el borde de foco sin tapar el dibujo de la app.
68const CROMO_FLOTANTE: usize = 8;
69
70/// Paso de la cascada con que se colocan las ventanas flotantes nuevas, en
71/// pixeles. Cada flotante se desplaza un paso respecto a la anterior, de modo
72/// que varias no se tapen por completo.
73const PASO_CASCADA: usize = 44;
74
75/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
76/// atiende la tarea del compositor desde el reactor cooperativo.
77#[derive(Clone, Copy)]
78pub enum Mando {
79    /// Ciclar al siguiente modo de teselado de `mirada-layout`.
80    CiclarLayout,
81    /// Mover el foco a la siguiente ventana viva.
82    FocoSiguiente,
83    /// Mover el foco a la ventana viva anterior.
84    FocoAnterior,
85    /// Promover la ventana enfocada a la posicion maestra del teselado.
86    Promover,
87    /// Mover la ventana enfocada una posicion adelante en el orden de teselado.
88    MoverAdelante,
89    /// Mover la ventana enfocada una posicion atras en el orden de teselado.
90    MoverAtras,
91    /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
92    Flotar,
93    /// Cerrar la aplicacion enfocada — una baja limpia, en vivo (Fase 10).
94    Cerrar,
95    /// Lanzar una aplicacion nueva — un alta en vivo (Fase 10).
96    Lanzar,
97}
98
99/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
100struct Ventana {
101    /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
102    natural_ancho: usize,
103    natural_alto: usize,
104    /// El marco actual — donde la app vive en pantalla. Si la ventana esta
105    /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
106    marco: RegionPantalla,
107    /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
108    /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
109    /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
110    /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
111    cache: Vec<u8>,
112    /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
113    /// solo ceros y no se recompone.
114    pintada: bool,
115    /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
116    /// vive; `Some(color)` la marca como muerta y la excluye del foco.
117    baliza: Option<Color>,
118    /// ¿Se ha pedido cerrar esta ventana en vivo (Fase 10)? Una vez `true`, su
119    /// app concluye su tarea, la ranura queda inerte —fuera del orden, del
120    /// orden-Z y del foco— y el teselado reclama su espacio.
121    cerrada: bool,
122}
123
124/// El escritorio: el registro de todas las ventanas y el modo de teselado.
125/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
126struct Escritorio {
127    modo: LayoutMode,
128    ancho: usize,
129    alto: usize,
130    /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
131    ventanas: Vec<Ventana>,
132    /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
133    /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
134    /// flotantes salen de aqui—. Separar el orden de la identidad permite
135    /// promover y reordenar ventanas sin tocar su `indice_app`.
136    orden: Vec<usize>,
137    /// Las ventanas FLOTANTES, en orden-Z (Fase 9): de atras hacia adelante.
138    /// `flotantes.last()` es la ventana frontal. Una ventana esta en `orden` o
139    /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
140    /// de `0..ventanas.len()`.
141    flotantes: Vec<usize>,
142}
143
144/// El escritorio global. Se funda una sola vez, en el arranque.
145static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
146
147/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
148/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
149/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
150static FOCO: AtomicUsize = AtomicUsize::new(0);
151
152/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
153/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
154/// compositor las drena desde el reactor cooperativo.
155static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
156
157/// Cuantos lanzamientos de aplicacion (Fase 10) aguardan. Lo incrementa
158/// `atender_mandos` al recibir un `Mando::Lanzar`; lo drena `partos_pendientes`,
159/// que lo lee el orquestador del kernel —el unico que sabe instanciar un WASM—.
160/// Atomico: el compositor lo escribe, el orquestador lo lee y lo pone a cero.
161static PARTOS: AtomicUsize = AtomicUsize::new(0);
162
163// =============================================================================
164//  Fundacion y consulta — el arranque
165// =============================================================================
166
167/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
168/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
169/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
170pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
171    MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
172
173    let mut ventanas = Vec::with_capacity(naturales.len());
174    for &(nat_ancho, nat_alto) in naturales {
175        ventanas.push(Ventana {
176            natural_ancho: nat_ancho,
177            natural_alto: nat_alto,
178            // Marco provisional; `aplicar_teselado` lo fija enseguida.
179            marco: RegionPantalla {
180                x: 0,
181                y: 0,
182                ancho: 0,
183                alto: 0,
184            },
185            // La cache: reservada UNA vez, acotada al lienzo natural.
186            cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
187            pintada: false,
188            baliza: None,
189            cerrada: false,
190        });
191    }
192
193    // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
194    // celda `i`. Ninguna ventana flota al nacer — el escritorio es puro
195    // teselado hasta que el teclado lo decida (`Alt+F`).
196    let orden = (0..ventanas.len()).collect();
197    let mut escritorio = Escritorio {
198        modo: MODO_INICIAL,
199        ancho,
200        alto,
201        ventanas,
202        orden,
203        flotantes: Vec::new(),
204    };
205    aplicar_teselado(&mut escritorio);
206
207    ESCRITORIO.call_once(|| Mutex::new(escritorio));
208}
209
210/// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
211/// `slot` del teselado va a la ventana `orden[slot]`: manda el orden, no la
212/// identidad. Las ventanas flotantes no estan en `orden` y conservan su marco.
213fn aplicar_teselado(escritorio: &mut Escritorio) {
214    let marcos = teselar(
215        escritorio.orden.len(),
216        escritorio.ancho,
217        escritorio.alto,
218        escritorio.modo,
219    );
220    for (slot, marco) in marcos.into_iter().enumerate() {
221        let ventana = escritorio.orden[slot];
222        escritorio.ventanas[ventana].marco = marco;
223    }
224}
225
226/// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
227/// antes de encender las apps: recompone el escritorio con todas las ventanas
228/// aun sin pintar — el teselado se ve como una rejilla de paneles.
229pub fn componer_escenario() {
230    let Some(escritorio) = ESCRITORIO.get() else {
231        return;
232    };
233    let escritorio = escritorio.lock();
234    recomponer(&escritorio);
235}
236
237/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
238/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
239pub fn foco() -> usize {
240    FOCO.load(Ordering::Relaxed)
241}
242
243/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
244/// cola lock-free es seguro en contexto de interrupcion.
245pub fn solicitar(mando: Mando) {
246    if let Some(mandos) = MANDOS.get() {
247        // Si la cola se desborda, el mando se pierde en silencio: mas vale
248        // perder una pulsacion que arriesgar un panico dentro de una IRQ.
249        let _ = mandos.push(mando);
250    }
251}
252
253// =============================================================================
254//  El fotograma de una app — cache y composicion
255// =============================================================================
256
257/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
258/// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
259/// flotantes ninguna ventana solapa a otra: basta repintar la que cambio —el
260/// camino RAPIDO—. Con flotantes vivas el solapamiento obliga a RECOMPONER el
261/// escritorio entero, respetando el orden-Z. Lo invoca la capacidad
262/// `sys_render_frame` desde el `tick` cooperativo.
263pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
264    let Some(escritorio) = ESCRITORIO.get() else {
265        return;
266    };
267    let mut escritorio = escritorio.lock();
268    {
269        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
270            return;
271        };
272        // Una ventana cerrada (Fase 10) ya no se pinta: su app pudo emitir un
273        // ultimo fotograma antes de que su tarea advirtiera la baja.
274        if ventana.cerrada {
275            return;
276        }
277        // Cachear el fotograma. El destino esta acotado al lienzo natural; se
278        // copia el minimo de ambas longitudes — jamas se desborda la cache.
279        let n = ventana.cache.len().min(datos.len());
280        ventana.cache[..n].copy_from_slice(&datos[..n]);
281        ventana.pintada = true;
282    }
283
284    if escritorio.flotantes.is_empty() {
285        // Camino RAPIDO: sin flotantes el escritorio es puro teselado y la app
286        // pinta directamente en su marco, como en la Fase 8.
287        let ventana = &escritorio.ventanas[indice];
288        let marco = ventana.marco;
289        let nat_ancho = ventana.natural_ancho;
290        let nat_alto = ventana.natural_alto;
291        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
292        drop(escritorio);
293        consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
294    } else {
295        // Hay ventanas flotantes: el solapamiento obliga a recomponer.
296        recomponer(&escritorio);
297    }
298}
299
300/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
301/// Desde aqui queda excluida del foco — el teclado la salta.
302pub fn desalojar(indice: usize, color: Color) {
303    let Some(escritorio) = ESCRITORIO.get() else {
304        return;
305    };
306    let mut escritorio = escritorio.lock();
307    {
308        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
309            return;
310        };
311        // Una ventana ya cerrada (Fase 10) no recibe baliza: la baja limpia
312        // gana a un desalojo que llegue tarde, en la misma vuelta.
313        if ventana.cerrada {
314            return;
315        }
316        ventana.baliza = Some(color);
317    }
318
319    if escritorio.flotantes.is_empty() {
320        let marco = escritorio.ventanas[indice].marco;
321        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
322        drop(escritorio);
323        consola::pintar_desalojo(marco, color, enfocada);
324    } else {
325        recomponer(&escritorio);
326    }
327}
328
329// =============================================================================
330//  Los mandos del teclado — el escritorio interactivo
331// =============================================================================
332
333/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
334/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
335/// seguro bloquear el `ESCRITORIO` y la consola.
336pub fn atender_mandos() {
337    let Some(mandos) = MANDOS.get() else {
338        return;
339    };
340    while let Some(mando) = mandos.pop() {
341        match mando {
342            Mando::CiclarLayout => ciclar_layout(),
343            Mando::FocoSiguiente => mover_foco(true),
344            Mando::FocoAnterior => mover_foco(false),
345            Mando::Promover => promover(),
346            Mando::MoverAdelante => mover_ventana(true),
347            Mando::MoverAtras => mover_ventana(false),
348            Mando::Flotar => flotar(),
349            Mando::Cerrar => cerrar(),
350            // El alta de una app necesita instanciar un WASM — algo que el
351            // compositor no sabe hacer—. Solo se cuenta la peticion; el
352            // orquestador del kernel la atendera (ver `partos_pendientes`).
353            Mando::Lanzar => {
354                PARTOS.fetch_add(1, Ordering::Relaxed);
355            }
356        }
357    }
358}
359
360/// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
361/// teseladas y recompone el escritorio entero desde las caches de respaldo.
362fn ciclar_layout() {
363    let Some(escritorio) = ESCRITORIO.get() else {
364        return;
365    };
366    let mut escritorio = escritorio.lock();
367    escritorio.modo = escritorio.modo.next();
368    aplicar_teselado(&mut escritorio);
369    recomponer(&escritorio);
370}
371
372/// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
373/// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
374/// desalojadas. Si la ventana recien enfocada flota, sube al frente del
375/// orden-Z: la flotante con el foco esta SIEMPRE delante.
376fn mover_foco(adelante: bool) {
377    let Some(escritorio) = ESCRITORIO.get() else {
378        return;
379    };
380    let mut escritorio = escritorio.lock();
381    // El recorrido del foco: las teseladas, luego las flotantes — un orden
382    // estable y visualmente coherente.
383    let recorrido: Vec<usize> = escritorio
384        .orden
385        .iter()
386        .chain(escritorio.flotantes.iter())
387        .copied()
388        .collect();
389    let n = recorrido.len();
390    if n == 0 {
391        return;
392    }
393    let anterior = FOCO.load(Ordering::Relaxed);
394    let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
395
396    // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
397    // `n` pasos se vuelve al punto de partida y el foco no cambia.
398    let mut nueva_pos = pos;
399    let mut nuevo = anterior;
400    for _ in 0..n {
401        nueva_pos = if adelante {
402            (nueva_pos + 1) % n
403        } else {
404            (nueva_pos + n - 1) % n
405        };
406        let candidata = recorrido[nueva_pos];
407        if escritorio.ventanas[candidata].baliza.is_none() {
408            nuevo = candidata;
409            break;
410        }
411    }
412    FOCO.store(nuevo, Ordering::Relaxed);
413    // La bocina pertenece a la ventana enfocada (Fase 12): al cambiar el foco,
414    // callar — la nueva dueña la reclamara en su proximo fotograma si quiere.
415    crate::drivers::altavoz::tono(0);
416    // La ventana recien enfocada, si flota, al frente del orden-Z.
417    alzar_si_flota(&mut escritorio, nuevo);
418    recomponer(&escritorio);
419}
420
421/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
422/// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
423/// el mando no hace nada — promover es una operacion del teselado.
424fn promover() {
425    let Some(escritorio) = ESCRITORIO.get() else {
426        return;
427    };
428    let mut escritorio = escritorio.lock();
429    let foco = FOCO.load(Ordering::Relaxed);
430    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
431        let ventana = escritorio.orden.remove(pos);
432        escritorio.orden.insert(0, ventana);
433        aplicar_teselado(&mut escritorio);
434        recomponer(&escritorio);
435    }
436}
437
438/// Mueve la ventana enfocada una posicion en el orden de teselado,
439/// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
440/// el mando no la afecta.
441fn mover_ventana(adelante: bool) {
442    let Some(escritorio) = ESCRITORIO.get() else {
443        return;
444    };
445    let mut escritorio = escritorio.lock();
446    let n = escritorio.orden.len();
447    if n < 2 {
448        return;
449    }
450    let foco = FOCO.load(Ordering::Relaxed);
451    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
452        let destino = if adelante {
453            (pos + 1) % n
454        } else {
455            (pos + n - 1) % n
456        };
457        escritorio.orden.swap(pos, destino);
458        aplicar_teselado(&mut escritorio);
459        recomponer(&escritorio);
460    }
461}
462
463// =============================================================================
464//  FASE 9 — orden-Z y ventanas flotantes
465// =============================================================================
466
467/// Alterna la ventana enfocada entre TESELADA y FLOTANTE. Al flotar, la ventana
468/// abandona el teselado —que se recalcula para las que quedan—, recibe un marco
469/// propio en cascada y sube al frente del orden-Z. Al volver al teselado, se
470/// reincorpora al final del orden. El foco no cambia: viaja con la ventana.
471fn flotar() {
472    let Some(escritorio) = ESCRITORIO.get() else {
473        return;
474    };
475    let mut escritorio = escritorio.lock();
476    let foco = FOCO.load(Ordering::Relaxed);
477
478    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
479        // Teselada -> flotante: se desliga del teselado, recibe su marco
480        // propio en cascada y sube al frente del orden-Z.
481        escritorio.orden.remove(pos);
482        let marco = marco_flotante(&escritorio, foco);
483        escritorio.ventanas[foco].marco = marco;
484        escritorio.flotantes.push(foco);
485        aplicar_teselado(&mut escritorio);
486        recomponer(&escritorio);
487    } else if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == foco) {
488        // Flotante -> teselada: vuelve a la rejilla, al final del orden.
489        escritorio.flotantes.remove(pos);
490        escritorio.orden.push(foco);
491        aplicar_teselado(&mut escritorio);
492        recomponer(&escritorio);
493    }
494}
495
496/// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
497/// de `flotantes`—. Si esta teselada, no hace nada.
498fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
499    if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
500        let ventana = escritorio.flotantes.remove(pos);
501        escritorio.flotantes.push(ventana);
502    }
503}
504
505/// El marco de una ventana recien hecha flotante: su lienzo natural mas un
506/// reborde de cromo, colocado en cascada —para que varias flotantes no se
507/// tapen del todo— y acotado al area de apps. Se invoca ANTES de inscribir la
508/// ventana en `flotantes`: su longitud da el escalon de la cascada.
509fn marco_flotante(escritorio: &Escritorio, indice: usize) -> RegionPantalla {
510    let area = area_apps(escritorio.ancho, escritorio.alto);
511    let ventana = &escritorio.ventanas[indice];
512    let ancho = (ventana.natural_ancho + 2 * CROMO_FLOTANTE).min(area.ancho);
513    let alto = (ventana.natural_alto + 2 * CROMO_FLOTANTE).min(area.alto);
514
515    // La cascada: un escalon por cada flotante ya existente.
516    let escalon = escritorio.flotantes.len().saturating_mul(PASO_CASCADA);
517    let mut x = area.x + 48 + escalon;
518    let mut y = area.y + 40 + escalon;
519    // Acotar: la ventana entera ha de caber dentro del area de apps.
520    if x + ancho > area.x + area.ancho {
521        x = area.x + area.ancho.saturating_sub(ancho);
522    }
523    if y + alto > area.y + area.alto {
524        y = area.y + area.alto.saturating_sub(alto);
525    }
526    RegionPantalla {
527        x,
528        y,
529        ancho,
530        alto,
531    }
532}
533
534/// Recompone el escritorio entero respetando el orden-Z. Arma la lista de capas
535/// —primero las ventanas TESELADAS, la capa de fondo; despues las FLOTANTES, de
536/// atras hacia adelante— y se la entrega a la consola, que las funde en ese
537/// orden de una sola pasada. El solapamiento se resuelve por el orden del
538/// pintado. La invocan los mandos del teclado y `presentar_fotograma` cuando
539/// hay flotantes vivas. El llamante sostiene ya el cerrojo del `ESCRITORIO`.
540fn recomponer(escritorio: &Escritorio) {
541    let area = area_apps(escritorio.ancho, escritorio.alto);
542    let foco = FOCO.load(Ordering::Relaxed);
543    let mut capas: Vec<Capa> = Vec::with_capacity(escritorio.ventanas.len());
544    for &indice in escritorio.orden.iter().chain(escritorio.flotantes.iter()) {
545        let ventana = &escritorio.ventanas[indice];
546        let contenido = match ventana.baliza {
547            Some(color) => Contenido::Baliza(color),
548            None if ventana.pintada => Contenido::Fotograma(&ventana.cache),
549            None => Contenido::Panel,
550        };
551        capas.push(Capa {
552            marco: ventana.marco,
553            nat_ancho: ventana.natural_ancho,
554            nat_alto: ventana.natural_alto,
555            contenido,
556            enfocada: indice == foco,
557        });
558    }
559    consola::recomponer(area, &capas);
560}
561
562// =============================================================================
563//  FASE 10 — alta y baja de aplicaciones en vivo
564// =============================================================================
565
566/// Cierra la aplicacion enfocada (`Alt+Q`): una baja LIMPIA, distinta del
567/// desalojo por falla. Marca la ventana como cerrada, libera su cache de
568/// respaldo, la saca del teselado y del orden-Z, y traslada el foco a una
569/// ventana viva contigua. La app, en su tarea, advertira la baja y concluira.
570fn cerrar() {
571    let Some(escritorio) = ESCRITORIO.get() else {
572        return;
573    };
574    let mut escritorio = escritorio.lock();
575    let foco = FOCO.load(Ordering::Relaxed);
576    // Solo se cierra una ventana viva. El foco jamas se posa en una muerta o
577    // cerrada, pero la guarda lo deja explicito.
578    match escritorio.ventanas.get(foco) {
579        Some(v) if v.baliza.is_none() && !v.cerrada => {}
580        _ => return,
581    }
582    // Marcar la baja y liberar el respaldo: la cache de un fotograma puede
583    // pesar un megabyte — no tiene sentido retenerla en una ranura inerte.
584    let ventana = &mut escritorio.ventanas[foco];
585    ventana.cerrada = true;
586    ventana.pintada = false;
587    ventana.cache = Vec::new();
588    // Sacarla del teselado y del orden-Z. El censo conserva la ranura —los
589    // indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
590    escritorio.orden.retain(|&v| v != foco);
591    escritorio.flotantes.retain(|&v| v != foco);
592    // El foco salta a la primera ventana viva que quede; si no queda ninguna,
593    // se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
594    let nuevo = escritorio
595        .orden
596        .iter()
597        .chain(escritorio.flotantes.iter())
598        .copied()
599        .find(|&v| {
600            let w = &escritorio.ventanas[v];
601            w.baliza.is_none() && !w.cerrada
602        })
603        .unwrap_or(foco);
604    FOCO.store(nuevo, Ordering::Relaxed);
605    // El foco cambia: callar la bocina (Fase 12) — ver `mover_foco`.
606    crate::drivers::altavoz::tono(0);
607    alzar_si_flota(&mut escritorio, nuevo);
608    aplicar_teselado(&mut escritorio);
609    recomponer(&escritorio);
610}
611
612/// Da de alta una ventana NUEVA y devuelve su indice —su identidad—. La crea
613/// con su cache de respaldo al tamaño natural, la añade al final del orden de
614/// teselado, recalcula el teselado y recompone. La invoca el orquestador del
615/// kernel justo antes de instanciar el WASM de la app, que necesita ese indice.
616pub fn nacer_ventana(nat_ancho: usize, nat_alto: usize) -> usize {
617    let Some(escritorio) = ESCRITORIO.get() else {
618        return 0;
619    };
620    let mut escritorio = escritorio.lock();
621    let indice = escritorio.ventanas.len();
622    escritorio.ventanas.push(Ventana {
623        natural_ancho: nat_ancho,
624        natural_alto: nat_alto,
625        marco: RegionPantalla {
626            x: 0,
627            y: 0,
628            ancho: 0,
629            alto: 0,
630        },
631        cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
632        pintada: false,
633        baliza: None,
634        cerrada: false,
635    });
636    escritorio.orden.push(indice);
637    aplicar_teselado(&mut escritorio);
638    recomponer(&escritorio);
639    indice
640}
641
642/// ¿Se ha pedido cerrar la ventana `indice`? Cada app la consulta en su tarea,
643/// fotograma a fotograma: cuando es `true`, concluye su tarea y se libera. Una
644/// ventana inexistente cuenta como cerrada.
645pub fn ventana_cerrada(indice: usize) -> bool {
646    let Some(escritorio) = ESCRITORIO.get() else {
647        return false;
648    };
649    escritorio
650        .lock()
651        .ventanas
652        .get(indice)
653        .map(|ventana| ventana.cerrada)
654        .unwrap_or(true)
655}
656
657/// Cuantas aplicaciones nuevas se han pedido lanzar desde la ultima consulta —y
658/// pone el contador a cero—. La invoca el orquestador del kernel —el unico que
659/// sabe instanciar un WASM— en cada fotograma de la tarea del compositor.
660pub fn partos_pendientes() -> usize {
661    PARTOS.swap(0, Ordering::Relaxed)
662}
663
664// =============================================================================
665//  Teselado — la geometria pura de `mirada-layout`
666// =============================================================================
667
668/// El area de pantalla que el compositor tesela: toda la pantalla menos la
669/// franja de la consola en la cima.
670pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
671    RegionPantalla {
672        x: 0,
673        y: FRANJA_CONSOLA.min(alto_pantalla),
674        ancho: ancho_pantalla,
675        alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
676    }
677}
678
679/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
680/// tiene exactamente `n` elementos, en el orden de las celdas del teselado.
681fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
682    let area = area_apps(ancho, alto);
683    let pantalla = Rect::new(
684        area.x as i32,
685        area.y as i32,
686        area.ancho as i32,
687        area.alto as i32,
688    );
689    let params = LayoutParams {
690        mode: modo,
691        gap: MARGEN,
692        ..LayoutParams::default()
693    };
694    tile(pantalla, n, &params)
695        .into_iter()
696        .map(rect_a_region)
697        .collect()
698}
699
700/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
701/// kernel (`usize`). Un rectangulo degenerado queda en cero.
702fn rect_a_region(r: Rect) -> RegionPantalla {
703    RegionPantalla {
704        x: r.x.max(0) as usize,
705        y: r.y.max(0) as usize,
706        ancho: r.w.max(0) as usize,
707        alto: r.h.max(0) as usize,
708    }
709}