gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main android/clear-screen-android/src/lib.rs Rust · 291 siq'ikuna · 10.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

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}