diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 54e868683696b6..68b4d443b785bf 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -10,6 +10,7 @@ use collections::{FxHashSet, HashMap}; use futures::channel::oneshot::Receiver; use raw_window_handle as rwh; +use util::ResultExt; use wayland_backend::client::ObjectId; use wayland_client::WEnum; use wayland_client::{Proxy, protocol::wl_surface}; @@ -1306,7 +1307,7 @@ impl PlatformWindow for WaylandWindow { fn draw(&self, scene: &Scene) { let mut state = self.borrow_mut(); - state.renderer.draw(scene); + state.renderer.draw(scene).log_err(); } fn completed_frame(&self) { diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 7766f23095fccf..0ecefa573776d5 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -227,26 +227,146 @@ impl X11ClientStatePtr { self.0.upgrade().map(X11Client) } - pub fn drop_window(&self, x_window: u32) { + /// Updates the GPU context for all windows after recovery + pub(crate) fn update_gpu_context(&self, context: crate::platform::wgpu::WgpuContext) { + if let Some(client) = self.get_client() { + client.0.borrow_mut().gpu_context = context; + } + } + + /// Orchestrates full GPU device recovery across all windows + pub(crate) fn recover_gpu(&self) -> anyhow::Result<()> { + use crate::platform::wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig}; + use anyhow::anyhow; + use std::sync::{Arc, mpsc}; + use std::time::Duration; + + let Some(client) = self.get_client() else { + return Err(anyhow!("Client state unavailable during GPU recovery")); + }; + + log::info!("Starting GPU recovery..."); + + let windows: Vec<_> = { + let state = client.0.borrow(); + state.windows + .values() + .map(|window_ref| window_ref.window.clone()) + .collect() + }; + + log::info!("Found {} windows to recover", windows.len()); + + for window in &windows { + window.pause_rendering(); + } + log::debug!("Paused rendering on all windows"); + + // This prevents the main thread from hanging if GPU init fails + let (sender, receiver) = mpsc::channel(); + std::thread::spawn(move || { + log::debug!("Creating new GPU context..."); + sender.send(WgpuContext::new()).log_err(); + }); + + let new_context = receiver + .recv_timeout(Duration::from_secs(10)) + .map_err(|_| anyhow!("GPU context creation timed out after 10 seconds"))??; + + log::info!( + "Created new GPU context with adapter: {:?}", + new_context.adapter.get_info().name + ); + + let atlases: Vec<_> = windows.iter().map(|window| window.get_atlas()).collect(); + log::debug!("Saved {} atlases", atlases.len()); + + for window in &windows { + window.prepare_atlas(); + } + log::debug!("Prepared atlases for recovery"); + + // In wgpu, surfaces are RAII, but we still mark them as invalid + for window in &windows { + window.destroy_surface(); + } + log::debug!("Destroyed old surfaces"); + + let renderer_params: Vec<_> = windows + .iter() + .map(|window| window.renderer_params()) + .collect(); + + let (sender, receiver) = mpsc::channel(); + let device = Arc::clone(&new_context.device); + let queue = Arc::clone(&new_context.queue); + let instance = new_context.instance.clone(); + let adapter = new_context.adapter.clone(); + + std::thread::spawn(move || { + log::debug!("Creating {} new renderers...", renderer_params.len()); + let result: anyhow::Result> = renderer_params + .into_iter() + .enumerate() + .map(|(index, (raw_window, size, transparent))| { + log::trace!("Creating renderer {} (size: {:?})", index, size); + let config = WgpuSurfaceConfig { size, transparent }; + WgpuRenderer::new_with_device_queue( + instance.clone(), + adapter.clone(), + Arc::clone(&device), + Arc::clone(&queue), + &raw_window, + config, + ) + }) + .collect(); + sender.send(result).log_err(); + }); + + let new_renderers = receiver + .recv_timeout(Duration::from_secs(10)) + .map_err(|_| anyhow!("Renderer creation timed out after 10 seconds"))??; + + log::info!("Created {} new renderers", new_renderers.len()); + + for ((window, renderer), atlas) in windows.iter().zip(new_renderers).zip(atlases) { + window.replace_renderer(renderer, &atlas); + } + log::debug!("Replaced renderers and adopted atlases"); + + self.update_gpu_context(new_context); + log::debug!("Updated client GPU context"); + + for window in &windows { + window.resume_rendering(); + } + log::debug!("Resumed rendering on all windows"); + + log::info!("GPU recovery successful for {} windows", windows.len()); + Ok(()) + } + + pub fn drop_window(&self, window_id: u32) { let Some(client) = self.get_client() else { return; }; let mut state = client.0.borrow_mut(); - if let Some(window_ref) = state.windows.remove(&x_window) + if let Some(window_ref) = state.windows.remove(&window_id) && let Some(RefreshState::PeriodicRefresh { event_loop_token, .. }) = window_ref.refresh_state { state.loop_handle.remove(event_loop_token); } - if state.mouse_focused_window == Some(x_window) { + if state.mouse_focused_window == Some(window_id) { state.mouse_focused_window = None; } - if state.keyboard_focused_window == Some(x_window) { + if state.keyboard_focused_window == Some(window_id) { state.keyboard_focused_window = None; } - state.cursor_styles.remove(&x_window); + state.cursor_styles.remove(&window_id); } pub fn update_ime_position(&self, bounds: Bounds) { @@ -779,6 +899,14 @@ impl X11Client { drop(state); window.close(); state = self.0.borrow_mut(); + } else if atom == state.atoms._GPUI_FORCE_UPDATE_WINDOW { + window.resume_rendering(); + drop(state); + window.refresh(crate::RequestFrameOptions { + force_render: true, + require_presentation: false, + }); + return Some(()); } else if atom == state.atoms._NET_WM_SYNC_REQUEST { window.state.borrow_mut().last_sync_counter = Some(x11rb::protocol::sync::Int64 { diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index cc48a86b0c3389..f03f0ef6a23080 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -78,6 +78,7 @@ x11rb::atom_manager! { _NET_WM_SYNC, _NET_SUPPORTED, _MOTIF_WM_HINTS, + _GPUI_FORCE_UPDATE_WINDOW, _GTK_SHOW_WINDOW_MENU, _GTK_FRAME_EXTENTS, _GTK_EDGE_CONSTRAINTS, @@ -225,7 +226,7 @@ fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { set } -struct RawWindow { +pub(crate) struct RawWindow { connection: *mut c_void, screen_id: usize, window_id: u32, @@ -916,9 +917,126 @@ impl X11Window { xcb_flush(&self.0.xcb); Ok(()) } + + /// Handles draw failures by attempting GPU recovery + fn handle_draw_failure(&self, error: anyhow::Error) { + log::error!("Renderer draw failed: {}", error); + + let inner = self.0.state.borrow(); + let client = inner.client.clone(); + let force_update_atom = inner.atoms._GPUI_FORCE_UPDATE_WINDOW; + drop(inner); + + // Attempt GPU recovery + if let Err(recovery_error) = client.recover_gpu() { + // Recovery failed - this is unrecoverable, panic to trigger crash reporting + panic!( + "GPU device lost (recovery failed: {}), original error: {}", + recovery_error, error + ); + } + + // Recovery succeeded - send force update to trigger redraw + self.send_force_update(force_update_atom); + } + + /// Sends a force update event to trigger window redraw after GPU recovery + fn send_force_update(&self, force_update_atom: xproto::Atom) { + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + force_update_atom, + [0, 0, 0, 0, 0], + ); + + check_reply( + || "X11 SendEvent for GPU recovery force update failed", + self.0.xcb.send_event( + false, + self.0.x_window, + EventMask::default(), + message, + ), + ) + .log_err(); + + xcb_flush(&self.0.xcb); + } } impl X11WindowStatePtr { + /// Pauses rendering (sets skip_draws flag) + pub(crate) fn pause_rendering(&self) { + let mut state = self.state.borrow_mut(); + state.renderer.pause_rendering(); + } + + /// Resumes rendering (clears skip_draws flag) + pub(crate) fn resume_rendering(&self) { + let mut state = self.state.borrow_mut(); + state.renderer.resume_rendering(); + } + + /// Destroys the surface (marks it as invalid for wgpu) + pub(crate) fn destroy_surface(&self) { + let mut state = self.state.borrow_mut(); + state.renderer.destroy_surface(); + } + + /// Gets renderer parameters needed for recreation + pub(crate) fn renderer_params( + &self, + ) -> ( + RawWindow, + crate::Size, + bool, + ) { + let state = self.state.borrow(); + let screen_index = state.display.id().0 as usize; + let visual_set = find_visuals(&self.xcb, screen_index); + let visual = visual_set.transparent.unwrap_or(visual_set.inherit); + + let raw_window = RawWindow { + connection: self.xcb.get_raw_xcb_connection(), + screen_id: screen_index, + window_id: self.x_window, + visual_id: visual.id, + }; + + let size = state.bounds.size; + let device_size = crate::Size { + width: crate::DevicePixels((size.width.0 * state.scale_factor) as i32), + height: crate::DevicePixels((size.height.0 * state.scale_factor) as i32), + }; + + let transparent = state.is_transparent(); + + (raw_window, device_size, transparent) + } + + /// Gets the current atlas (for preservation during recovery) + pub(crate) fn get_atlas(&self) -> std::sync::Arc { + let state = self.state.borrow(); + std::sync::Arc::clone(state.renderer.sprite_atlas()) + } + + /// Prepares atlas for recovery (clears without destroying) + pub(crate) fn prepare_atlas(&self) { + let state = self.state.borrow(); + state.renderer.prepare_atlas(); + } + + /// Replaces the renderer with a new one and adopts the atlas + pub(crate) fn replace_renderer( + &self, + mut new_renderer: crate::platform::wgpu::WgpuRenderer, + atlas: &std::sync::Arc, + ) { + new_renderer.adopt_atlas(atlas); + let mut state = self.state.borrow_mut(); + state.renderer = new_renderer; + } + pub fn should_close(&self) -> bool { let mut cb = self.callbacks.borrow_mut(); if let Some(mut should_close) = cb.should_close.take() { @@ -1558,7 +1676,10 @@ impl PlatformWindow for X11Window { fn draw(&self, scene: &Scene) { let mut inner = self.0.state.borrow_mut(); - inner.renderer.draw(scene); + if let Err(error) = inner.renderer.draw(scene) { + drop(inner); + self.handle_draw_failure(error); + } } fn sprite_atlas(&self) -> Arc { diff --git a/crates/gpui_wgpu/src/wgpu_atlas.rs b/crates/gpui_wgpu/src/wgpu_atlas.rs index d3614ea126e3d3..3d7b2cdf990759 100644 --- a/crates/gpui_wgpu/src/wgpu_atlas.rs +++ b/crates/gpui_wgpu/src/wgpu_atlas.rs @@ -55,12 +55,36 @@ impl WgpuAtlas { lock.flush_uploads(); } - pub fn get_texture_info(&self, id: AtlasTextureId) -> WgpuTextureInfo { + /// Clears atlas state without destroying GPU resources (for device loss recovery) + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub(crate) fn handle_device_lost(&self) { + let mut lock = self.0.lock(); + lock.storage.clear_without_destroy(); + lock.tiles_by_key.clear(); + lock.pending_uploads.clear(); + } + + /// Updates the GPU context after device recovery + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub(crate) fn update_gpu_context( + &self, + new_device: Arc, + new_queue: Arc, + ) { + let mut lock = self.0.lock(); + lock.device = new_device; + lock.queue = new_queue; + } + + /// Returns texture info, or None if the texture ID is invalid + /// (can happen after GPU recovery when old scene frames reference cleared atlas) + pub fn get_texture_info(&self, id: AtlasTextureId) -> Option { let lock = self.0.lock(); - let texture = &lock.storage[id]; - WgpuTextureInfo { + let textures = &lock.storage[id.kind]; + let texture = textures.textures.get(id.index as usize)?.as_ref()?; + Some(WgpuTextureInfo { view: texture.view.clone(), - } + }) } } @@ -178,19 +202,19 @@ impl WgpuAtlasState { live_atlas_keys: 0, }; - if let Some(ix) = index { - texture_list.textures[ix] = Some(atlas_texture); + if let Some(index) = index { + texture_list.textures[index] = Some(atlas_texture); texture_list .textures - .get_mut(ix) - .and_then(|t| t.as_mut()) + .get_mut(index) + .and_then(|texture| texture.as_mut()) .expect("texture must exist") } else { texture_list.textures.push(Some(atlas_texture)); texture_list .textures .last_mut() - .and_then(|t| t.as_mut()) + .and_then(|texture| texture.as_mut()) .expect("texture must exist") } } @@ -242,6 +266,19 @@ struct WgpuAtlasStorage { polychrome_textures: AtlasTextureList, } +impl WgpuAtlasStorage { + /// Clears all textures without destroying them (for device loss recovery) + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + fn clear_without_destroy(&mut self) { + self.monochrome_textures.textures.clear(); + self.subpixel_textures.textures.clear(); + self.polychrome_textures.textures.clear(); + self.monochrome_textures.free_list.clear(); + self.subpixel_textures.free_list.clear(); + self.polychrome_textures.free_list.clear(); + } +} + impl ops::Index for WgpuAtlasStorage { type Output = AtlasTextureList; fn index(&self, kind: AtlasTextureKind) -> &Self::Output { diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index f443f12dd54e59..b003ce46b8acb7 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -7,6 +7,7 @@ use gpui::{ }; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::num::NonZeroU64; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[repr(C)] @@ -115,42 +116,48 @@ pub struct WgpuRenderer { adapter_info: wgpu::AdapterInfo, transparent_alpha_mode: wgpu::CompositeAlphaMode, opaque_alpha_mode: wgpu::CompositeAlphaMode, + skip_draws: bool, + device_lost: Arc, } impl WgpuRenderer { + fn monitor_device_loss( + device: &Arc, + log_suffix: &'static str, + ) -> Arc { + let device_lost = Arc::new(AtomicBool::new(false)); + let device_clone = Arc::clone(device); + let flag = Arc::clone(&device_lost); + + std::thread::spawn(move || { + smol::block_on(async { + let reason = device_clone.lost().await; + flag.store(true, Ordering::SeqCst); + log::error!( + "GPU device lost detected by wgpu{}: {:?}", + log_suffix, + reason + ); + }); + }); + + device_lost + } + /// Creates a new WgpuRenderer from raw window handles. /// /// # Safety /// The caller must ensure that the window handle remains valid for the lifetime /// of the returned renderer. - pub fn new( - context: &WgpuContext, - window: &W, + fn new_internal( + device: Arc, + queue: Arc, + adapter: &wgpu::Adapter, + surface: wgpu::Surface<'static>, config: WgpuSurfaceConfig, - ) -> anyhow::Result { - let window_handle = window - .window_handle() - .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?; - let display_handle = window - .display_handle() - .map_err(|e| anyhow::anyhow!("Failed to get display handle: {e}"))?; - - let target = wgpu::SurfaceTargetUnsafe::RawHandle { - raw_display_handle: display_handle.as_raw(), - raw_window_handle: window_handle.as_raw(), - }; - - // Safety: The caller guarantees that the window handle is valid for the - // lifetime of this renderer. In practice, the RawWindow struct is created - // from the native window handles and the surface is dropped before the window. - let surface = unsafe { - context - .instance - .create_surface_unsafe(target) - .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))? - }; - - let surface_caps = surface.get_capabilities(&context.adapter); + log_suffix: &'static str, + ) -> Self { + let surface_caps = surface.get_capabilities(adapter); // Prefer standard 8-bit non-sRGB formats that don't require special features. // Other formats like Rgba16Unorm require TEXTURE_FORMAT_16BIT_NORM which may // not be available on all devices. @@ -200,13 +207,13 @@ impl WgpuRenderer { alpha_mode, view_formats: vec![], }; - surface.configure(&context.device, &surface_config); + surface.configure(&device, &surface_config); - let device = Arc::clone(&context.device); - let queue = Arc::clone(&context.queue); - let dual_source_blending = context.supports_dual_source_blending(); + let dual_source_blending = adapter + .features() + .contains(wgpu::Features::DUAL_SOURCE_BLENDING); - let rendering_params = RenderingParameters::new(&context.adapter, surface_format); + let rendering_params = RenderingParameters::new(adapter, surface_format); let bind_group_layouts = Self::create_bind_group_layouts(&device); let pipelines = Self::create_pipelines( &device, @@ -310,9 +317,11 @@ impl WgpuRenderer { ], }); - let adapter_info = context.adapter.get_info(); + let adapter_info = adapter.get_info(); - Ok(Self { + let device_lost = Self::monitor_device_loss(&device, log_suffix); + + Self { device, queue, surface, @@ -338,7 +347,119 @@ impl WgpuRenderer { adapter_info, transparent_alpha_mode, opaque_alpha_mode, - }) + skip_draws: false, + device_lost, + } + } + + pub fn new( + context: &WgpuContext, + window: &W, + config: WgpuSurfaceConfig, + ) -> anyhow::Result { + let window_handle = window + .window_handle() + .map_err(|error| anyhow::anyhow!("Failed to get window handle: {error}"))?; + let display_handle = window + .display_handle() + .map_err(|error| anyhow::anyhow!("Failed to get display handle: {error}"))?; + + let target = wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: display_handle.as_raw(), + raw_window_handle: window_handle.as_raw(), + }; + + // Safety: The caller guarantees that the window handle is valid for the + // lifetime of this renderer. In practice, the RawWindow struct is created + // from the native window handles and the surface is dropped before the window. + let surface = unsafe { + context + .instance + .create_surface_unsafe(target) + .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))? + }; + + Ok(Self::new_internal( + context.device.clone(), + context.queue.clone(), + &context.adapter, + surface, + config, + "", + )) + } + + /// Creates a new renderer using existing device and queue (for GPU recovery) + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn new_with_device_queue( + instance: wgpu::Instance, + adapter: wgpu::Adapter, + device: Arc, + queue: Arc, + window: &W, + config: WgpuSurfaceConfig, + ) -> anyhow::Result { + let window_handle = window + .window_handle() + .map_err(|error| anyhow::anyhow!("Failed to get window handle: {error}"))?; + let display_handle = window + .display_handle() + .map_err(|error| anyhow::anyhow!("Failed to get display handle: {error}"))?; + + let target = wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: display_handle.as_raw(), + raw_window_handle: window_handle.as_raw(), + }; + + let surface = unsafe { + instance + .create_surface_unsafe(target) + .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))? + }; + + Ok(Self::new_internal( + device, + queue, + &adapter, + surface, + config, + " (recovered device)", + )) + } + + /// Pauses rendering (used during GPU recovery) + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn pause_rendering(&mut self) { + self.skip_draws = true; + } + + /// Resumes rendering after GPU recovery + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn resume_rendering(&mut self) { + self.skip_draws = false; + } + + /// Prepares atlas for GPU recovery by clearing it without destroying resources + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn prepare_atlas(&self) { + self.atlas.handle_device_lost(); + } + + /// Adopts a new atlas after GPU recovery + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn adopt_atlas(&mut self, atlas: &Arc) { + atlas.update_gpu_context( + Arc::clone(&self.device), + Arc::clone(&self.queue), + ); + self.atlas = Arc::clone(atlas); + } + + /// Destroys the surface (no-op for wgpu, surface is RAII) + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + pub fn destroy_surface(&mut self) { + // In wgpu, surfaces are automatically cleaned up when dropped + // We just need to ensure we don't try to use it after this point } fn create_bind_group_layouts(device: &wgpu::Device) -> WgpuBindGroupLayouts { @@ -822,18 +943,42 @@ impl WgpuRenderer { } } - pub fn draw(&mut self, scene: &Scene) { - self.atlas.before_frame(); + pub fn draw(&mut self, scene: &Scene) -> anyhow::Result<()> { + if self.skip_draws { + return Ok(()); + } + + if self.device_lost.load(Ordering::SeqCst) { + log::error!("Device loss detected by background monitor"); + return Err(anyhow::anyhow!("GPU device was lost")); + } + self.atlas.before_frame(); let frame = match self.surface.get_current_texture() { Ok(frame) => frame, - Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { + Err(wgpu::SurfaceError::Outdated) => { + log::debug!("Surface outdated, reconfiguring"); self.surface.configure(&self.device, &self.surface_config); - return; + return Ok(()); } - Err(e) => { - log::error!("Failed to acquire surface texture: {e}"); - return; + Err(wgpu::SurfaceError::Lost) => { + log::warn!("Surface lost, reconfiguring"); + self.surface.configure(&self.device, &self.surface_config); + match self.surface.get_current_texture() { + Ok(frame) => frame, + Err(_) => { + return Err(anyhow::anyhow!( + "Surface lost and cannot be reconfigured - possible device loss" + )); + } + } + } + Err(wgpu::SurfaceError::Timeout) => { + log::warn!("Surface timeout, skipping frame"); + return Ok(()); + } + Err(wgpu::SurfaceError::OutOfMemory) => { + return Err(anyhow::anyhow!("GPU out of memory")); } }; let frame_view = frame @@ -1002,7 +1147,7 @@ impl WgpuRenderer { self.instance_buffer_capacity ); frame.present(); - return; + return Ok(()); } self.grow_instance_buffer(); continue; @@ -1010,7 +1155,7 @@ impl WgpuRenderer { self.queue.submit(std::iter::once(encoder.finish())); frame.present(); - return; + return Ok(()); } } @@ -1069,7 +1214,9 @@ impl WgpuRenderer { instance_offset: &mut u64, pass: &mut wgpu::RenderPass<'_>, ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); + let Some(tex_info) = self.atlas.get_texture_info(texture_id) else { + return true; // Skip if texture not found + }; let data = unsafe { Self::instance_bytes(sprites) }; self.draw_instances_with_texture( data, @@ -1088,7 +1235,9 @@ impl WgpuRenderer { instance_offset: &mut u64, pass: &mut wgpu::RenderPass<'_>, ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); + let Some(tex_info) = self.atlas.get_texture_info(texture_id) else { + return true; // Skip if texture not found + }; let data = unsafe { Self::instance_bytes(sprites) }; let pipeline = self .pipelines @@ -1112,7 +1261,9 @@ impl WgpuRenderer { instance_offset: &mut u64, pass: &mut wgpu::RenderPass<'_>, ) -> bool { - let tex_info = self.atlas.get_texture_info(texture_id); + let Some(tex_info) = self.atlas.get_texture_info(texture_id) else { + return true; // Skip if texture not found + }; let data = unsafe { Self::instance_bytes(sprites) }; self.draw_instances_with_texture( data,