dear_imgui_wgpu/texture.rs
1//! Texture management for the WGPU renderer
2//!
3//! This module handles texture creation, updates, and management,
4//! integrating with Dear ImGui's modern texture system.
5
6use crate::{RendererError, RendererResult};
7use dear_imgui::{TextureData, TextureFormat as ImGuiTextureFormat, TextureId, TextureStatus};
8use std::collections::HashMap;
9use wgpu::*;
10
11/// Result of a texture update operation
12///
13/// This enum represents the outcome of a texture update operation and
14/// contains any state changes that need to be applied to the texture data.
15/// This follows Rust's principle of explicit state management.
16#[derive(Debug, Clone)]
17pub enum TextureUpdateResult {
18 /// Texture was successfully created
19 Created { texture_id: TextureId },
20 /// Texture was successfully updated
21 Updated,
22 /// Texture was destroyed
23 Destroyed,
24 /// Texture update failed
25 Failed,
26 /// No action was needed
27 NoAction,
28}
29
30impl TextureUpdateResult {
31 /// Apply the result to a texture data object
32 ///
33 /// This method updates the texture data's status and ID based on the operation result.
34 /// This is the Rust-idiomatic way to handle state updates.
35 pub fn apply_to(self, texture_data: &mut TextureData) {
36 match self {
37 TextureUpdateResult::Created { texture_id } => {
38 texture_data.set_tex_id(texture_id);
39 texture_data.set_status(TextureStatus::OK);
40 }
41 TextureUpdateResult::Updated => {
42 texture_data.set_status(TextureStatus::OK);
43 }
44 TextureUpdateResult::Destroyed => {
45 texture_data.set_status(TextureStatus::Destroyed);
46 }
47 TextureUpdateResult::Failed => {
48 texture_data.set_status(TextureStatus::Destroyed);
49 }
50 TextureUpdateResult::NoAction => {
51 // No changes needed
52 }
53 }
54 }
55}
56
57/// WGPU texture resource
58///
59/// This corresponds to ImGui_ImplWGPU_Texture in the C++ implementation
60#[derive(Debug)]
61pub struct WgpuTexture {
62 /// WGPU texture object
63 pub texture: Texture,
64 /// Texture view for binding
65 pub texture_view: TextureView,
66}
67
68impl WgpuTexture {
69 /// Create a new WGPU texture
70 pub fn new(texture: Texture, texture_view: TextureView) -> Self {
71 Self {
72 texture,
73 texture_view,
74 }
75 }
76
77 /// Get the texture view for binding
78 pub fn view(&self) -> &TextureView {
79 &self.texture_view
80 }
81
82 /// Get the texture object
83 pub fn texture(&self) -> &Texture {
84 &self.texture
85 }
86}
87
88/// Texture manager for WGPU renderer
89///
90/// This manages the mapping between Dear ImGui texture IDs and WGPU textures,
91/// similar to the ImageBindGroups storage in the C++ implementation.
92#[derive(Debug, Default)]
93pub struct WgpuTextureManager {
94 /// Map from texture ID to WGPU texture
95 textures: HashMap<u64, WgpuTexture>,
96 /// Next available texture ID
97 next_id: u64,
98}
99
100impl WgpuTextureManager {
101 /// Create a new texture manager
102 pub fn new() -> Self {
103 Self {
104 textures: HashMap::new(),
105 next_id: 1, // Start from 1, 0 is reserved for null texture
106 }
107 }
108
109 /// Register a new texture and return its ID
110 pub fn register_texture(&mut self, texture: WgpuTexture) -> u64 {
111 let id = self.next_id;
112 self.next_id += 1;
113 self.textures.insert(id, texture);
114 id
115 }
116
117 /// Get a texture by ID
118 pub fn get_texture(&self, id: u64) -> Option<&WgpuTexture> {
119 self.textures.get(&id)
120 }
121
122 /// Remove a texture by ID
123 pub fn remove_texture(&mut self, id: u64) -> Option<WgpuTexture> {
124 self.textures.remove(&id)
125 }
126
127 /// Check if a texture exists
128 pub fn contains_texture(&self, id: u64) -> bool {
129 self.textures.contains_key(&id)
130 }
131
132 /// Insert a texture with a specific ID
133 pub fn insert_texture_with_id(&mut self, id: u64, texture: WgpuTexture) {
134 self.textures.insert(id, texture);
135 // Update next_id if necessary
136 if id >= self.next_id {
137 self.next_id = id + 1;
138 }
139 }
140
141 /// Destroy a texture by ID
142 pub fn destroy_texture_by_id(&mut self, id: u64) {
143 self.remove_texture(id);
144 }
145
146 /// Update an existing texture from Dear ImGui texture data with specific ID
147 pub fn update_texture_from_data_with_id(
148 &mut self,
149 device: &Device,
150 queue: &Queue,
151 texture_data: &TextureData,
152 texture_id: u64,
153 ) -> RendererResult<()> {
154 // For WGPU, we recreate the texture instead of updating in place
155 // This is simpler and more reliable than trying to update existing textures
156 if self.contains_texture(texture_id) {
157 // Remove old texture
158 self.remove_texture(texture_id);
159
160 // Create new texture
161 let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
162
163 // Move the texture to the correct ID slot if needed
164 if new_texture_id != texture_id
165 && let Some(texture) = self.remove_texture(new_texture_id)
166 {
167 self.insert_texture_with_id(texture_id, texture);
168 }
169
170 Ok(())
171 } else {
172 Err(RendererError::InvalidTextureId(texture_id))
173 }
174 }
175
176 /// Get the number of registered textures
177 pub fn texture_count(&self) -> usize {
178 self.textures.len()
179 }
180
181 /// Clear all textures
182 pub fn clear(&mut self) {
183 self.textures.clear();
184 self.next_id = 1;
185 }
186}
187
188/// Texture creation and management functions
189impl WgpuTextureManager {
190 /// Create a texture from Dear ImGui texture data
191 pub fn create_texture_from_data(
192 &mut self,
193 device: &Device,
194 queue: &Queue,
195 texture_data: &TextureData,
196 ) -> RendererResult<u64> {
197 let width = texture_data.width() as u32;
198 let height = texture_data.height() as u32;
199 let format = texture_data.format();
200
201 let pixels = texture_data
202 .pixels()
203 .ok_or_else(|| RendererError::BadTexture("No pixel data available".to_string()))?;
204
205 // Convert ImGui texture format to WGPU format and handle data conversion
206 // This matches the texture format handling in imgui_impl_wgpu.cpp
207 let (wgpu_format, converted_data, _bytes_per_pixel) = match format {
208 ImGuiTextureFormat::RGBA32 => {
209 // RGBA32 maps directly to RGBA8Unorm (matches C++ implementation)
210 if pixels.len() != (width * height * 4) as usize {
211 return Err(RendererError::BadTexture(format!(
212 "RGBA32 texture data size mismatch: expected {} bytes, got {}",
213 width * height * 4,
214 pixels.len()
215 )));
216 }
217 (TextureFormat::Rgba8Unorm, pixels.to_vec(), 4u32)
218 }
219 ImGuiTextureFormat::Alpha8 => {
220 // Convert Alpha8 to RGBA32 for WGPU (white RGB + original alpha)
221 // This ensures compatibility with the standard RGBA8Unorm format
222 if pixels.len() != (width * height) as usize {
223 return Err(RendererError::BadTexture(format!(
224 "Alpha8 texture data size mismatch: expected {} bytes, got {}",
225 width * height,
226 pixels.len()
227 )));
228 }
229 let mut rgba_data = Vec::with_capacity(pixels.len() * 4);
230 for &alpha in pixels {
231 rgba_data.extend_from_slice(&[255, 255, 255, alpha]); // White RGB + alpha
232 }
233 (TextureFormat::Rgba8Unorm, rgba_data, 4u32)
234 }
235 };
236
237 // Create WGPU texture (matches the descriptor setup in imgui_impl_wgpu.cpp)
238 if cfg!(debug_assertions) {
239 tracing::debug!(
240 target: "dear-imgui-wgpu",
241 "[dear-imgui-wgpu][debug] Create texture: {}x{} format={:?}",
242 width, height, format
243 );
244 }
245 let texture = device.create_texture(&TextureDescriptor {
246 label: Some("Dear ImGui Texture"),
247 size: Extent3d {
248 width,
249 height,
250 depth_or_array_layers: 1,
251 },
252 mip_level_count: 1,
253 sample_count: 1,
254 dimension: TextureDimension::D2,
255 format: wgpu_format,
256 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
257 view_formats: &[],
258 });
259
260 // Validate texture data size before upload
261 let expected_size = (width * height * 4) as usize; // Always RGBA after conversion
262 if converted_data.len() != expected_size {
263 return Err(RendererError::BadTexture(format!(
264 "Converted texture data size mismatch: expected {} bytes, got {}",
265 expected_size,
266 converted_data.len()
267 )));
268 }
269
270 // Upload texture data (matches the upload logic in imgui_impl_wgpu.cpp)
271 // WebGPU requires bytes_per_row to be 256-byte aligned. Pad rows if needed.
272 let bpp = 4u32;
273 let unpadded_bytes_per_row = width * bpp;
274 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
275 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
276 if padded_bytes_per_row == unpadded_bytes_per_row {
277 // Aligned: direct upload
278 queue.write_texture(
279 wgpu::TexelCopyTextureInfo {
280 texture: &texture,
281 mip_level: 0,
282 origin: wgpu::Origin3d::ZERO,
283 aspect: wgpu::TextureAspect::All,
284 },
285 &converted_data,
286 wgpu::TexelCopyBufferLayout {
287 offset: 0,
288 bytes_per_row: Some(unpadded_bytes_per_row),
289 rows_per_image: Some(height),
290 },
291 Extent3d {
292 width,
293 height,
294 depth_or_array_layers: 1,
295 },
296 );
297 } else {
298 // Pad each row to the required alignment
299 let mut padded: Vec<u8> = vec![0; (padded_bytes_per_row * height) as usize];
300 for row in 0..height as usize {
301 let src_off = row * (unpadded_bytes_per_row as usize);
302 let dst_off = row * (padded_bytes_per_row as usize);
303 padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)].copy_from_slice(
304 &converted_data[src_off..src_off + (unpadded_bytes_per_row as usize)],
305 );
306 }
307 queue.write_texture(
308 wgpu::TexelCopyTextureInfo {
309 texture: &texture,
310 mip_level: 0,
311 origin: wgpu::Origin3d::ZERO,
312 aspect: wgpu::TextureAspect::All,
313 },
314 &padded,
315 wgpu::TexelCopyBufferLayout {
316 offset: 0,
317 bytes_per_row: Some(padded_bytes_per_row),
318 rows_per_image: Some(height),
319 },
320 Extent3d {
321 width,
322 height,
323 depth_or_array_layers: 1,
324 },
325 );
326 if cfg!(debug_assertions) {
327 tracing::debug!(
328 target: "dear-imgui-wgpu",
329 "[dear-imgui-wgpu][debug] Upload texture with padded row pitch: unpadded={} padded={}",
330 unpadded_bytes_per_row, padded_bytes_per_row
331 );
332 }
333 }
334
335 // Create texture view
336 let texture_view = texture.create_view(&TextureViewDescriptor::default());
337
338 // Create WGPU texture wrapper
339 let wgpu_texture = WgpuTexture::new(texture, texture_view);
340
341 // Register and return ID
342 let texture_id = self.register_texture(wgpu_texture);
343 if cfg!(debug_assertions) {
344 tracing::debug!(
345 target: "dear-imgui-wgpu",
346 "[dear-imgui-wgpu][debug] Texture registered: id={}",
347 texture_id
348 );
349 }
350 Ok(texture_id)
351 }
352
353 /// Update an existing texture from Dear ImGui texture data
354 pub fn update_texture_from_data(
355 &mut self,
356 device: &Device,
357 queue: &Queue,
358 texture_data: &TextureData,
359 ) -> RendererResult<()> {
360 let texture_id = texture_data.tex_id().id();
361
362 // For WGPU, we recreate the texture instead of updating in place
363 // This is simpler and more reliable than trying to update existing textures
364 if self.contains_texture(texture_id) {
365 // Remove old texture
366 self.remove_texture(texture_id);
367
368 // Create new texture
369 let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
370
371 // Move the texture to the correct ID slot if needed
372 if new_texture_id != texture_id
373 && let Some(texture) = self.remove_texture(new_texture_id)
374 {
375 self.insert_texture_with_id(texture_id, texture);
376 }
377 } else {
378 // Create new texture if it doesn't exist
379 let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
380 if new_texture_id != texture_id
381 && let Some(texture) = self.remove_texture(new_texture_id)
382 {
383 self.insert_texture_with_id(texture_id, texture);
384 }
385 }
386
387 Ok(())
388 }
389
390 /// Destroy a texture
391 pub fn destroy_texture(&mut self, texture_id: TextureId) {
392 let texture_id_u64 = texture_id.id();
393 self.remove_texture(texture_id_u64);
394 // WGPU textures are automatically cleaned up when dropped
395 }
396
397 /// Handle texture updates from Dear ImGui draw data
398 ///
399 /// This iterates `DrawData::textures()` and applies create/update/destroy requests.
400 /// For `WantCreate`, we create the GPU texture, then write the generated id back into
401 /// the `ImTextureData` via `set_tex_id()` and mark status `OK` (matching C++ backend).
402 /// For `WantUpdates`, if a valid id is not yet assigned (first use), we create now and
403 /// assign the id; otherwise we update in place.
404 pub fn handle_texture_updates(
405 &mut self,
406 draw_data: &dear_imgui::render::DrawData,
407 device: &Device,
408 queue: &Queue,
409 ) {
410 for texture_data in draw_data.textures() {
411 let status = texture_data.status();
412 let current_tex_id = texture_data.tex_id().id();
413
414 match status {
415 TextureStatus::WantCreate => {
416 // Create and upload new texture to graphics system
417 // Following the official imgui_impl_wgpu.cpp implementation
418
419 match self.create_texture_from_data(device, queue, texture_data) {
420 Ok(wgpu_texture_id) => {
421 // CRITICAL: Set the texture ID back to Dear ImGui
422 // In the C++ implementation, they use the TextureView pointer as ImTextureID.
423 // In Rust, we can't get the raw pointer, so we use our internal texture ID.
424 // This works because our renderer will map the texture ID to the WGPU texture.
425 let new_texture_id = dear_imgui::TextureId::from(wgpu_texture_id);
426
427 texture_data.set_tex_id(new_texture_id);
428
429 // Mark texture as ready
430 texture_data.set_status(TextureStatus::OK);
431 }
432 Err(e) => {
433 println!(
434 "Failed to create texture for ID: {}, error: {}",
435 current_tex_id, e
436 );
437 }
438 }
439 }
440 TextureStatus::WantUpdates => {
441 let imgui_tex_id = texture_data.tex_id();
442 let internal_id = imgui_tex_id.id();
443
444 // If we don't have a valid texture id yet (first update) or the
445 // id isn't registered, create it now and write back the TexID,
446 // so this frame (or the next) can bind the correct texture.
447 if internal_id == 0 || !self.contains_texture(internal_id) {
448 match self.create_texture_from_data(device, queue, texture_data) {
449 Ok(new_id) => {
450 texture_data.set_tex_id(dear_imgui::TextureId::from(new_id));
451 texture_data.set_status(TextureStatus::OK);
452 }
453 Err(_e) => {
454 // Leave it destroyed to avoid retry storm; user can request create again
455 texture_data.set_status(TextureStatus::Destroyed);
456 }
457 }
458 } else if self
459 .update_texture_from_data_with_id(device, queue, texture_data, internal_id)
460 .is_err()
461 {
462 // If update fails, mark as destroyed
463 texture_data.set_status(TextureStatus::Destroyed);
464 } else {
465 texture_data.set_status(TextureStatus::OK);
466 }
467 }
468 TextureStatus::WantDestroy => {
469 // Only destroy when unused frames > 0 (align with official backend behavior)
470 let mut can_destroy = true;
471 unsafe {
472 let raw = texture_data.as_raw();
473 if !raw.is_null() {
474 // If field not present in bindings on some versions, default true
475 #[allow(unused_unsafe)]
476 {
477 // Access UnusedFrames if available
478 // SAFETY: reading a plain field from raw C struct
479 can_destroy = (*raw).UnusedFrames > 0;
480 }
481 }
482 }
483 if can_destroy {
484 let imgui_tex_id = texture_data.tex_id();
485 let internal_id = imgui_tex_id.id();
486 // Remove from cache
487 self.remove_texture(internal_id);
488 texture_data.set_status(TextureStatus::Destroyed);
489 }
490 }
491 TextureStatus::OK | TextureStatus::Destroyed => {
492 // No action needed
493 }
494 }
495 }
496 }
497
498 /// Update a single texture based on its status
499 ///
500 /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
501 ///
502 /// # Returns
503 ///
504 /// Returns a `TextureUpdateResult` that contains the operation result and
505 /// any status/ID updates that need to be applied to the texture data.
506 /// This follows Rust's principle of explicit state management.
507 ///
508 /// # Example
509 ///
510 /// ```rust,no_run
511 /// # use dear_imgui_wgpu::*;
512 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
513 /// # let mut texture_manager = WgpuTextureManager::new();
514 /// # let device = todo!();
515 /// # let queue = todo!();
516 /// # let mut texture_data = dear_imgui::TextureData::new();
517 /// let result = texture_manager.update_single_texture(&texture_data, &device, &queue)?;
518 /// result.apply_to(&mut texture_data);
519 /// # Ok(())
520 /// # }
521 /// ```
522 pub fn update_single_texture(
523 &mut self,
524 texture_data: &dear_imgui::TextureData,
525 device: &Device,
526 queue: &Queue,
527 ) -> Result<TextureUpdateResult, String> {
528 match texture_data.status() {
529 TextureStatus::WantCreate => {
530 match self.create_texture_from_data(device, queue, texture_data) {
531 Ok(texture_id) => Ok(TextureUpdateResult::Created {
532 texture_id: TextureId::from(texture_id),
533 }),
534 Err(e) => Err(format!("Failed to create texture: {}", e)),
535 }
536 }
537 TextureStatus::WantUpdates => {
538 let internal_id = texture_data.tex_id().id();
539 if internal_id == 0 || !self.contains_texture(internal_id) {
540 // No valid ID yet: create now and return Created so caller can set TexID
541 match self.create_texture_from_data(device, queue, texture_data) {
542 Ok(texture_id) => Ok(TextureUpdateResult::Created {
543 texture_id: TextureId::from(texture_id),
544 }),
545 Err(e) => Err(format!("Failed to create texture: {}", e)),
546 }
547 } else {
548 match self.update_texture_from_data_with_id(
549 device,
550 queue,
551 texture_data,
552 internal_id,
553 ) {
554 Ok(_) => Ok(TextureUpdateResult::Updated),
555 Err(_e) => Ok(TextureUpdateResult::Failed),
556 }
557 }
558 }
559 TextureStatus::WantDestroy => {
560 self.destroy_texture(texture_data.tex_id());
561 Ok(TextureUpdateResult::Destroyed)
562 }
563 TextureStatus::OK | TextureStatus::Destroyed => Ok(TextureUpdateResult::NoAction),
564 }
565 }
566}