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}