tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
feat: llimphi standalone — framework UI soberano extraído del monorepo
1//! Demo Tier 1 Android: pinta la pantalla con LEAD_GRAY usando llimphi-hal.
2//!
3//! Logging exhaustivo en cada paso del bootstrap para diagnosticar
4//! cuelgues en device real desde `adb logcat -s llimphi-android:V`.
5//! Panic hook captura backtraces a logcat — sin esto el crash es
6//! invisible (Android cierra el proceso silenciosamente).
7//!
8//! Orden de inicialización en `resumed`:
9//! 1. crear Window via winit
10//! 2. crear wgpu::Instance
11//! 3. crear Surface con la NativeWindow
12//! 4. request_adapter pasándole compatible_surface=Some(&surface)
13//! 5. request_device
14//! 6. configurar surface (formato, tamaño)
15//! 7. crear textura intermedia + blitter (llimphi-hal::WinitSurface)
16//!
17//! El orden 3 antes que 4 es lo que **garantiza** que el adapter
18//! elegido sabe presentar a esa NativeWindow concreta. Llamar
19//! `Hal::new(None)` (como hacía la primera versión) elige un adapter
20//! "cualquiera" y después la creación de surface puede fallar — o
21//! peor, parecer OK y crashear en el primer `present`.
22
23use std::sync::Arc;
24use std::time::Instant;
25
26use llimphi_hal::winit::application::ApplicationHandler;
27use llimphi_hal::winit::event::WindowEvent;
28use llimphi_hal::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
29use llimphi_hal::winit::window::{Window, WindowAttributes, WindowId};
30use llimphi_hal::{wgpu, Hal, Surface, WinitSurface};
31
32const LEAD_GRAY: wgpu::Color = wgpu::Color {
33 r: 0.235,
34 g: 0.239,
35 b: 0.247,
36 a: 1.0,
37};
38
39const TAG: &str = "llimphi-android";
40
41struct State {
42 window: Arc<Window>,
43 hal: Hal,
44 surface: WinitSurface,
45}
46
47struct App {
48 state: Option<State>,
49 frames: u64,
50 last_report: Instant,
51}
52
53impl App {
54 fn new() -> Self {
55 Self {
56 state: None,
57 frames: 0,
58 last_report: Instant::now(),
59 }
60 }
61
62 /// Bootstrap: crea el estado completo o devuelve un mensaje
63 /// explicando dónde falló. **No panic-ea** — los panics en
64 /// `android_main` arrancan la cierre del proceso antes que el
65 /// logcat flushee.
66 fn boot(&self, event_loop: &ActiveEventLoop) -> Result<State, String> {
67 log::info!("[boot] 1/7 creando Window");
68 let window = event_loop
69 .create_window(WindowAttributes::default().with_title("llimphi · clear_screen"))
70 .map_err(|e| format!("create_window: {e}"))?;
71 let window = Arc::new(window);
72 let size = window.inner_size();
73 log::info!(
74 "[boot] window ok · inner_size = {}x{}",
75 size.width,
76 size.height
77 );
78
79 log::info!("[boot] 2/7 creando wgpu::Instance");
80 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
81 backends: wgpu::Backends::all(),
82 ..Default::default()
83 });
84 log::info!("[boot] instance ok · backends activos = {:?}", instance);
85
86 log::info!("[boot] 3/7 creando Surface contra la NativeWindow");
87 let surface = instance
88 .create_surface(window.clone())
89 .map_err(|e| format!("create_surface: {e}"))?;
90 log::info!("[boot] surface creada");
91
92 log::info!("[boot] 4/7 request_adapter (compatible_surface=Some)");
93 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
94 power_preference: wgpu::PowerPreference::HighPerformance,
95 force_fallback_adapter: false,
96 compatible_surface: Some(&surface),
97 }))
98 .ok_or_else(|| "request_adapter devolvió None — sin GPU compatible".to_string())?;
99 let info = adapter.get_info();
100 log::info!(
101 "[boot] adapter ok · backend={:?} name={:?} driver={:?}",
102 info.backend,
103 info.name,
104 info.driver_info
105 );
106
107 log::info!("[boot] 5/7 request_device");
108 // En Android (Mali/Adreno entry-level) Limits::default suele exceder
109 // el hardware. using_resolution recorta lo recortable preservando
110 // los counts mínimos (5 storage buffers/stage que vello necesita).
111 let limits = wgpu::Limits::default().using_resolution(adapter.limits());
112 let (device, queue) = pollster::block_on(adapter.request_device(
113 &wgpu::DeviceDescriptor {
114 label: Some("clear-screen-android-device"),
115 required_features: wgpu::Features::empty(),
116 required_limits: limits,
117 memory_hints: wgpu::MemoryHints::Performance,
118 },
119 None,
120 ))
121 .map_err(|e| format!("request_device: {e}"))?;
122 log::info!("[boot] device + queue ok");
123
124 log::info!("[boot] 6/7 ensamblando Hal");
125 let hal = Hal {
126 instance,
127 adapter,
128 device,
129 queue,
130 };
131
132 log::info!("[boot] 7/7 envolviendo en WinitSurface (intermediate + blitter)");
133 // Crítico: usar `from_surface` (no `new`), pasando la surface que
134 // ya creamos en el paso 3. `WinitSurface::new` haría un segundo
135 // create_surface contra la misma NativeWindow y Android responde
136 // ERROR_NATIVE_WINDOW_IN_USE_KHR → panic.
137 let llimphi_surface = WinitSurface::from_surface(&hal, window.clone(), surface)
138 .map_err(|e| format!("WinitSurface::from_surface: {e}"))?;
139 log::info!("[boot] ✓ bootstrap completo, pidiendo redraw");
140 window.request_redraw();
141
142 Ok(State {
143 window,
144 hal,
145 surface: llimphi_surface,
146 })
147 }
148}
149
150impl ApplicationHandler for App {
151 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
152 log::info!("Resumed event");
153 match self.boot(event_loop) {
154 Ok(state) => self.state = Some(state),
155 Err(e) => {
156 log::error!("BOOT FAILED: {e}");
157 // No exit-amos para que el process siga vivo y se vea el
158 // log; el usuario cerrará la app manualmente.
159 }
160 }
161 }
162
163 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
164 log::info!("Suspended event — liberando surface");
165 self.state = None;
166 }
167
168 fn window_event(
169 &mut self,
170 event_loop: &ActiveEventLoop,
171 _id: WindowId,
172 event: WindowEvent,
173 ) {
174 let Some(state) = self.state.as_mut() else {
175 return;
176 };
177 match event {
178 WindowEvent::CloseRequested => {
179 log::info!("CloseRequested");
180 event_loop.exit();
181 }
182 WindowEvent::Resized(size) => {
183 log::info!("Resized → {}x{}", size.width, size.height);
184 state.surface.resize(size.width, size.height);
185 state.window.request_redraw();
186 }
187 WindowEvent::RedrawRequested => {
188 let frame = match state.surface.acquire() {
189 Ok(f) => f,
190 Err(e) => {
191 log::warn!("acquire falló ({e}); reconfigurando");
192 let (w, h) = state.surface.size();
193 state.surface.resize(w, h);
194 state.window.request_redraw();
195 return;
196 }
197 };
198 let mut encoder =
199 state
200 .hal
201 .device
202 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
203 label: Some("clear_screen-encoder"),
204 });
205 {
206 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
207 label: Some("clear_screen-pass"),
208 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
209 view: frame.view(),
210 resolve_target: None,
211 ops: wgpu::Operations {
212 load: wgpu::LoadOp::Clear(LEAD_GRAY),
213 store: wgpu::StoreOp::Store,
214 },
215 })],
216 depth_stencil_attachment: None,
217 timestamp_writes: None,
218 occlusion_query_set: None,
219 });
220 }
221 state.hal.queue.submit(std::iter::once(encoder.finish()));
222 state.surface.present(frame, &state.hal);
223
224 self.frames += 1;
225 let elapsed = self.last_report.elapsed();
226 if elapsed.as_secs() >= 1 {
227 let fps = self.frames as f64 / elapsed.as_secs_f64();
228 log::info!("{fps:.1} fps");
229 self.frames = 0;
230 self.last_report = Instant::now();
231 }
232 state.window.request_redraw();
233 }
234 _ => {}
235 }
236 }
237}
238
239#[cfg(target_os = "android")]
240fn install_panic_logger() {
241 // Sin esto los panic son invisibles: Android mata el proceso antes
242 // que la línea de stderr llegue a logcat. set_hook redirige el panic
243 // info a log::error que sí sale en logcat (vía android_logger).
244 std::panic::set_hook(Box::new(|info| {
245 let payload = info
246 .payload()
247 .downcast_ref::<&str>()
248 .copied()
249 .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
250 .unwrap_or("<unknown panic payload>");
251 let location = info
252 .location()
253 .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
254 .unwrap_or_else(|| "<unknown location>".into());
255 log::error!("PANIC at {location} — {payload}");
256 // Forzar flush stdio del android_logger (mejor que nada).
257 }));
258}
259
260#[cfg(target_os = "android")]
261#[no_mangle]
262fn android_main(app: android_activity::AndroidApp) {
263 android_logger::init_once(
264 android_logger::Config::default()
265 .with_max_level(log::LevelFilter::Debug)
266 .with_tag(TAG),
267 );
268 install_panic_logger();
269
270 log::info!("android_main START");
271
272 use llimphi_hal::winit::event_loop::EventLoopBuilder;
273 use llimphi_hal::winit::platform::android::EventLoopBuilderExtAndroid;
274
275 let event_loop: EventLoop<()> = match EventLoopBuilder::default().with_android_app(app).build()
276 {
277 Ok(el) => el,
278 Err(e) => {
279 log::error!("EventLoop::build failed: {e}");
280 return;
281 }
282 };
283 event_loop.set_control_flow(ControlFlow::Poll);
284 log::info!("event_loop construido, entrando a run_app");
285
286 let mut app_handler = App::new();
287 if let Err(e) = event_loop.run_app(&mut app_handler) {
288 log::error!("run_app: {e}");
289 }
290 log::info!("android_main END");
291}