feat(renaser): Fases 8b y 8c — el escritorio interactivo

El compositor de la 8a teselaba, pero era inmovil. Las 8b/8c lo hacen
vivo: el teclado reordena el escritorio y mueve el foco en caliente.

- Cache de fotogramas: cada ventana guarda en RAM del kernel su ultimo
  fotograma —reservada una vez, acotada al lienzo natural—. Al re-teselar
  o mover el foco, el kernel recompone desde la cache: las apps que solo
  pintan en init (cronista) conservan su imagen sin enterarse del cambio.
- compositor: el registro ESCRITORIO (ventanas, marcos, caches, modo);
  presentar_fotograma, desalojar, atender_mandos, ciclar_layout,
  mover_foco. Foco en un AtomicUsize, mandos en una cola lock-free.
- teclado: la IRQ1 deja de difundir. Alt es el modificador del sistema —
  Alt+Espacio cicla el teselado, Alt+J/K mueven el foco—; una tecla
  ordinaria va SOLO a la app enfocada (CANALES reindexado por indice_app).
- consola: borde de foco (indigo / gris) en cada marco.

Guardarrail anti-interbloqueo: la IRQ1 jamas bloquea ESCRITORIO; se
comunica por dos atomicos y una cola lock-free. Las caches se reservan
una sola vez, al tamaño natural — sin asignacion en el bucle del reactor.

Verificado en QEMU (screendump + sendkey): arranque teselado con hola
enfocada; Alt+Espacio cicla a CenteredMaster y las apps estaticas
conservan su contenido; Alt+J mueve el foco; las teclas llegan solo a la
app enfocada. Cierra la Fase 8 — el compositor teselante e interactivo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 19:19:21 +00:00
parent e94023d8af
commit 5c462e6d30
12 changed files with 676 additions and 177 deletions
+10 -12
View File
@@ -29,18 +29,15 @@ use wasmi::{Caller, Error, Extern, Linker, Memory, StoreLimits};
use crate::almacen::Hash;
use crate::async_system::teclado::CanalTeclado;
use crate::grafico::RegionPantalla;
/// El estado del host adscrito al `Store` de una aplicacion: cuanto necesita
/// una capacidad para servir a ESA app y a ninguna otra — su region de pantalla,
/// su canal de teclado y sus cuotas de recursos. Dos apps jamas comparten nada.
pub(crate) struct ContextoCapacidades {
/// El marco que el compositor (Fase 8) asigno a la app — el rectangulo de
/// pantalla donde vive. El kernel centra en el el fotograma de la app.
pub(crate) marco: RegionPantalla,
/// El tamaño natural del lienzo de la app, en pixeles. El fotograma que
/// entrega `sys_render_frame` mide exactamente `natural_ancho × natural_alto`;
/// el compositor lo coloca, sin deformarlo, dentro del `marco`.
/// el compositor lo cachea y lo compone, sin deformarlo, en el marco que el
/// teselado le asigno.
pub(crate) natural_ancho: usize,
pub(crate) natural_alto: usize,
/// El canal de teclado propio de la aplicacion.
@@ -48,9 +45,9 @@ pub(crate) struct ContextoCapacidades {
/// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
/// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
pub(crate) limites: StoreLimits,
/// El indice de esta app en el Manifiesto de Genesis — su identidad. Las
/// capacidades de estado (Fase 7c) lo usan para hallar la `EntradaApp`
/// correcta: cada app persiste en SU ranura, jamas en la de otra.
/// El indice de esta app — su identidad. La usan las capacidades de estado
/// (Fase 7c) para hallar su `EntradaApp` del manifiesto, y el compositor
/// (Fase 8) para hallar su ventana en el escritorio: jamas la de otra.
pub(crate) indice_app: usize,
}
@@ -98,7 +95,7 @@ pub(crate) fn enlazar_capacidades(
"renaser",
"sys_render_frame",
|caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> {
let marco = caller.data().marco;
let indice = caller.data().indice_app;
let nat_ancho = caller.data().natural_ancho;
let nat_alto = caller.data().natural_alto;
@@ -124,9 +121,10 @@ pub(crate) fn enlazar_capacidades(
"WASM :: sys_render_frame desbordo la memoria lineal del modulo",
)?;
// Limites verificados: el compositor centra el fotograma natural
// de la app dentro del marco que el teselado le asigno.
crate::volcar_marco_wasm(marco, nat_ancho, nat_alto, fotograma);
// Limites verificados: el compositor cachea el fotograma —para
// poder recomponerlo si el escritorio se re-tesela— y lo compone,
// centrado, en el marco que el teselado asigno a esta app.
crate::compositor::presentar_fotograma(indice, fotograma);
Ok(())
},
)?;
+25 -34
View File
@@ -20,7 +20,7 @@ use wasmi::{
CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc,
};
use crate::grafico::{Color, RegionPantalla};
use crate::grafico::Color;
use env::ContextoCapacidades;
/// Combustible concedido a `init`. Cubre con holgura el pintado inicial del
@@ -64,34 +64,28 @@ impl FallaApp {
/// aqui la instancia se conserva y el kernel la hace avanzar `tick` a `tick`.
pub struct AplicacionWasm {
/// El almacen: todo el estado de ESTA instancia — su memoria lineal, sus
/// globales y el contexto de capacidades con su region de pantalla.
/// globales y el contexto de capacidades con su identidad e indice.
almacen: Store<ContextoCapacidades>,
/// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
/// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
/// el handle de la `Instance` no aporta nada y no se retiene.
func_tick: TypedFunc<(), ()>,
/// El marco que el compositor asigno a la app — su ventana en pantalla, y
/// donde se tatua su baliza de desalojo si llega a fallar.
marco: RegionPantalla,
}
impl AplicacionWasm {
/// Carga, valida, instancia y arranca una aplicacion WASM aislada, ligada a
/// una region de pantalla. Si algo falla en el camino, se devuelve la falla
/// en lugar de incendiar el kernel.
/// Carga, valida, instancia y arranca una aplicacion WASM aislada. Si algo
/// falla en el camino, se devuelve la falla en lugar de incendiar el kernel.
///
/// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
/// el reactor en cada pulso del reloj.
/// El ABI del userspace exige dos exportaciones: `init` —invocada una sola
/// vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por el
/// reactor en cada pulso del reloj.
///
/// `marco` es el rectangulo que el compositor (Fase 8) asigno a la app;
/// `natural_ancho`/`natural_alto`, el tamaño de su lienzo. `techo_memoria`
/// es su cuota de memoria lineal —la dicta su `EntradaApp` del manifiesto—,
/// e `indice_app` su posicion en el: su identidad para las capacidades de
/// estado persistido (Fase 7c).
/// `natural_ancho`/`natural_alto` son el tamaño del lienzo de la app;
/// `techo_memoria`, su cuota de memoria lineal —la dicta su `EntradaApp` del
/// manifiesto—; e `indice_app`, su identidad: la posicion con que el
/// compositor halla su ventana y las capacidades de estado su ranura.
pub fn cargar(
bytecode: &[u8],
marco: RegionPantalla,
natural_ancho: usize,
natural_alto: usize,
techo_memoria: usize,
@@ -108,10 +102,10 @@ impl AplicacionWasm {
// 2. Validar y traducir el modulo — ya instrumentado con fuel.
let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?;
// 3. El almacen, con el contexto de capacidades de ESTA app: su region
// de pantalla, su canal de teclado y su techo de memoria. El canal
// se crea ahora pero se inscribe en la difusion de la IRQ1 al final,
// ya con la app cargada: una carga fallida no deja canales huerfanos.
// 3. El almacen, con el contexto de capacidades de ESTA app: su lienzo
// natural, su canal de teclado y su techo de memoria. El canal se
// crea ahora pero se inscribe en el censo de la IRQ1 al final, ya con
// la app cargada: una carga fallida no deja canales huerfanos.
let canal = crate::async_system::teclado::crear_canal();
let limites = StoreLimitsBuilder::new()
.memory_size(techo_memoria)
@@ -122,7 +116,6 @@ impl AplicacionWasm {
let mut almacen = Store::new(
&motor,
ContextoCapacidades {
marco,
natural_ancho,
natural_alto,
canal,
@@ -161,14 +154,11 @@ impl AplicacionWasm {
.map_err(|_| FallaApp::Carga)?;
// 8. Con la app ya cargada e instanciada, inscribir su canal de teclado
// en la difusion de la IRQ1: desde aqui recibe cada pulsacion.
crate::async_system::teclado::registrar_canal(&almacen.data().canal);
// en el censo de la IRQ1, en la ranura de su `indice_app`: desde
// aqui recibe las teclas cuando el compositor le da el foco.
crate::async_system::teclado::registrar_canal(indice_app, &almacen.data().canal);
Ok(AplicacionWasm {
almacen,
func_tick,
marco,
})
Ok(AplicacionWasm { almacen, func_tick })
}
/// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de
@@ -194,10 +184,11 @@ impl AplicacionWasm {
}
}
/// El marco de pantalla que el compositor asigno a la aplicacion — donde
/// se tatua su baliza si el kernel llega a desalojarla.
pub fn marco(&self) -> RegionPantalla {
self.marco
/// El indice de la aplicacion — su identidad en el escritorio del
/// compositor. Lo usa la tarea de la app para decirle al compositor que
/// ventana desalojar si la app llega a fallar.
pub fn indice(&self) -> usize {
self.almacen.data().indice_app
}
}
@@ -207,6 +198,6 @@ impl AplicacionWasm {
/// empujando scancodes a una cola muerta: una fuga lenta pero segura.
impl Drop for AplicacionWasm {
fn drop(&mut self) {
crate::async_system::teclado::cerrar_canal(&self.almacen.data().canal);
crate::async_system::teclado::cerrar_canal(self.almacen.data().indice_app);
}
}