//! UI/kernel bridge construction. //! //! The UI side owns: //! - a gamepad snapshot cell (writer) + connected flag (writer), //! - a shared staging buffer that the CPU thread fills with the guest's //! frontbuffer bytes on each `VdSwap`, //! - an `EventLoopProxy` to wake winit with a redraw request, //! - a shutdown atomic the UI flips on window close. //! //! The kernel side gets a [`xenia_kernel::UiBridge`] made from closures over //! the same shared state, so it can read gamepad + post swaps from the CPU //! thread without knowing about winit. use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Mutex; use crossbeam_utils::atomic::AtomicCell; use winit::event_loop::EventLoopProxy; use xenia_gpu::texture_cache::TextureKey; use xenia_gpu::xenos_constants::XenosConstantsBlock; use xenia_hid::GamepadState; use xenia_kernel::{SwapInfo, UiBridge}; use xenia_memory::MemoryAccess; /// Snapshot of the guest frontbuffer, copied out of guest memory on each /// `VdSwap` so the UI thread can upload it without touching the CPU thread's /// [`xenia_memory::GuestMemory`]. #[derive(Default)] pub struct FrontbufferSnapshot { /// Width of the most-recently-swapped frontbuffer, in pixels. pub width: u32, /// Height in pixels. pub height: u32, /// Tightly-packed RGBA8 bytes (`width * height * 4`). Decoded from the /// raw guest bytes by the kernel-side closure (see [`build`]). pub rgba: Vec, /// Monotonic counter so the UI can tell new frames apart from stale /// re-reads. pub revision: u64, /// The most recent swap metadata. Kept alongside the bytes for HUD use. pub info: Option, } /// The UI-side half of the bridge. Installed into the [`App`](crate::app) /// that runs on the main thread. pub struct UiHandles { pub gamepad: Arc>, pub gamepad_connected: Arc, pub shutdown: Arc, pub frontbuffer: Arc>, /// Live-updated summed CPU instruction counter. The `xenia-app` run /// loop stores `cycle_count` into this every ~10k instructions so the /// HUD keeps animating even before the first `VdSwap` fires. Authoritative /// counter comes from scheduler HW-thread contexts; this atomic is just /// a cross-thread cache. pub instructions_counter: Arc, /// P3b assets: cloned by `vd_swap` via `UiBridge::publish_assets`. /// The UI redraw path reads them (mutex-guarded) alongside the /// corresponding `SwapInfo` and passes them to the Xenos pipeline. pub shader_blobs: Arc>>>, pub xenos_constants: Arc>, /// P5 primary texture — `None` means "no texture this frame, use /// the magenta stub"; `Some((key, bytes))` means the kernel decoded /// fetch-constant slot 0 into linear bytes that the UI should /// upload into the host cache and bind at `@group(1) @binding(0)`. pub primary_texture: Arc)>>>, } /// Swap event posted by the CPU-side `VdSwap` handler via /// [`EventLoopProxy::send_event`] after the frontbuffer bytes have been /// copied into [`UiHandles::frontbuffer`]. #[derive(Clone, Copy, Debug)] pub struct SwapEvent(pub SwapInfo); /// Build a paired ([`UiHandles`], [`UiBridge`]). /// /// The proxy is the winit user-event injector for a freshly-constructed /// `EventLoop`; `xenia-app` owns the event loop and keeps the /// `UiHandles` alongside it. pub fn build(proxy: EventLoopProxy) -> (UiHandles, UiBridge) { let gamepad = Arc::new(AtomicCell::new(GamepadState::default())); let gamepad_connected = Arc::new(AtomicBool::new(false)); let shutdown = Arc::new(AtomicBool::new(false)); let frontbuffer = Arc::new(Mutex::new(FrontbufferSnapshot::default())); let instructions_counter = Arc::new(AtomicU64::new(0)); let shader_blobs = Arc::new(Mutex::new(HashMap::>::new())); let xenos_constants = Arc::new(Mutex::new(XenosConstantsBlock::default())); let primary_texture: Arc)>>> = Arc::new(Mutex::new(None)); let kernel_bridge = UiBridge { gamepad: { let gp = Arc::clone(&gamepad); Arc::new(move || gp.load()) }, // `post_swap` now only bumps the SwapInfo + revision; the RGBA // bytes arrive via `publish_frontbuffer` earlier in the same // `VdSwap` (P4 replaced the raw-scrape path with a CPU-side // detile). Posting the swap event after the bytes land guarantees // the UI's redraw path sees the latest pixels. post_swap: { let proxy = proxy.clone(); let fb = Arc::clone(&frontbuffer); Arc::new(move |info: SwapInfo, _mem: &dyn MemoryAccess| { if let Ok(mut lock) = fb.lock() { lock.info = Some(info); lock.revision = lock.revision.wrapping_add(1); } let _ = proxy.send_event(SwapEvent(info)); }) }, shutdown: Arc::clone(&shutdown), gamepad_connected: Arc::clone(&gamepad_connected), instructions_counter: Arc::clone(&instructions_counter), publish_xenos_assets: { let blobs = Arc::clone(&shader_blobs); let consts = Arc::clone(&xenos_constants); Arc::new(move |new_blobs, new_consts| { if let Ok(mut g) = blobs.lock() { *g = new_blobs; } if let Ok(mut g) = consts.lock() { *g = new_consts; } }) }, publish_frontbuffer: { let fb = Arc::clone(&frontbuffer); Arc::new(move |w, h, bytes| { if let Ok(mut lock) = fb.lock() { lock.width = w; lock.height = h; lock.rgba = bytes; } }) }, publish_texture: { let tex = Arc::clone(&primary_texture); Arc::new(move |payload| { if let Ok(mut lock) = tex.lock() { *lock = payload; } }) }, }; let handles = UiHandles { gamepad, gamepad_connected, shutdown, frontbuffer, instructions_counter, shader_blobs, xenos_constants, primary_texture, }; (handles, kernel_bridge) } impl UiHandles { pub fn request_shutdown(&self) { self.shutdown.store(true, Ordering::Relaxed); } }