Skip to main content

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::{RenderResources, RendererError, RendererResult};
7use dear_imgui_rs::{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)]
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    /// Custom samplers registered for external textures (sampler_id -> sampler)
99    custom_samplers: HashMap<u64, Sampler>,
100    /// Mapping from texture_id -> sampler_id for per-texture custom sampling
101    custom_sampler_by_texture: HashMap<u64, u64>,
102    /// Cached common bind groups (uniform buffer + sampler) per sampler_id
103    common_bind_groups: HashMap<u64, BindGroup>,
104    /// Next available sampler ID
105    next_sampler_id: u64,
106}
107
108impl Default for WgpuTextureManager {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl WgpuTextureManager {
115    /// Convert a sub-rectangle of ImGui texture pixels into a tightly packed RGBA8 buffer
116    fn convert_subrect_to_rgba(
117        texture_data: &TextureData,
118        rect: dear_imgui_rs::texture::TextureRect,
119    ) -> Option<Vec<u8>> {
120        let pixels = texture_data.pixels()?;
121        let tex_w = texture_data.width() as usize;
122        let tex_h = texture_data.height() as usize;
123        if tex_w == 0 || tex_h == 0 {
124            return None;
125        }
126
127        let bpp = texture_data.bytes_per_pixel() as usize;
128        let (rx, ry, rw, rh) = (
129            rect.x as usize,
130            rect.y as usize,
131            rect.w as usize,
132            rect.h as usize,
133        );
134        if rw == 0 || rh == 0 || rx >= tex_w || ry >= tex_h {
135            return None;
136        }
137
138        // Clamp to texture bounds defensively
139        let rw = rw.min(tex_w.saturating_sub(rx));
140        let rh = rh.min(tex_h.saturating_sub(ry));
141
142        let mut out = vec![0u8; rw * rh * 4];
143        match texture_data.format() {
144            ImGuiTextureFormat::RGBA32 => {
145                for row in 0..rh {
146                    let src_off = ((ry + row) * tex_w + rx) * bpp;
147                    let dst_off = row * rw * 4;
148                    // Copy only the row slice and convert layout if needed (it is already RGBA)
149                    out[dst_off..dst_off + rw * 4]
150                        .copy_from_slice(&pixels[src_off..src_off + rw * 4]);
151                }
152            }
153            ImGuiTextureFormat::Alpha8 => {
154                for row in 0..rh {
155                    let src_off = ((ry + row) * tex_w + rx) * bpp; // bpp = 1
156                    let dst_off = row * rw * 4;
157                    for i in 0..rw {
158                        let a = pixels[src_off + i];
159                        let dst = &mut out[dst_off + i * 4..dst_off + i * 4 + 4];
160                        dst.copy_from_slice(&[255, 255, 255, a]);
161                    }
162                }
163            }
164        }
165        Some(out)
166    }
167
168    /// Apply queued sub-rectangle updates to an existing WGPU texture.
169    /// Returns true if any update was applied.
170    fn apply_subrect_updates(
171        &mut self,
172        queue: &Queue,
173        texture_data: &TextureData,
174        texture_id: u64,
175    ) -> RendererResult<bool> {
176        let wgpu_tex = match self.textures.get(&texture_id) {
177            Some(t) => t,
178            None => return Ok(false),
179        };
180
181        // Collect update rectangles; prefer explicit Updates[] if present,
182        // otherwise fallback to single UpdateRect.
183        let mut rects: Vec<dear_imgui_rs::texture::TextureRect> = texture_data.updates().collect();
184        if rects.is_empty() {
185            let r = texture_data.update_rect();
186            if r.w > 0 && r.h > 0 {
187                rects.push(r);
188            }
189        }
190        if rects.is_empty() {
191            return Ok(false);
192        }
193
194        // Upload each rect
195        for rect in rects {
196            if let Some(tight_rgba) = Self::convert_subrect_to_rgba(texture_data, rect) {
197                let width = rect.w as u32;
198                let height = rect.h as u32;
199                let bpp = 4u32;
200                let unpadded_bytes_per_row = width * bpp;
201                let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
202                let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
203
204                if padded_bytes_per_row == unpadded_bytes_per_row {
205                    // Aligned: direct upload
206                    queue.write_texture(
207                        wgpu::TexelCopyTextureInfo {
208                            texture: wgpu_tex.texture(),
209                            mip_level: 0,
210                            origin: wgpu::Origin3d {
211                                x: rect.x as u32,
212                                y: rect.y as u32,
213                                z: 0,
214                            },
215                            aspect: wgpu::TextureAspect::All,
216                        },
217                        &tight_rgba,
218                        wgpu::TexelCopyBufferLayout {
219                            offset: 0,
220                            bytes_per_row: Some(unpadded_bytes_per_row),
221                            rows_per_image: Some(height),
222                        },
223                        wgpu::Extent3d {
224                            width,
225                            height,
226                            depth_or_array_layers: 1,
227                        },
228                    );
229                } else {
230                    // Pad each row to the required alignment
231                    let mut padded = vec![0u8; (padded_bytes_per_row * height) as usize];
232                    for row in 0..height as usize {
233                        let src_off = row * (unpadded_bytes_per_row as usize);
234                        let dst_off = row * (padded_bytes_per_row as usize);
235                        padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)]
236                            .copy_from_slice(
237                                &tight_rgba[src_off..src_off + (unpadded_bytes_per_row as usize)],
238                            );
239                    }
240                    queue.write_texture(
241                        wgpu::TexelCopyTextureInfo {
242                            texture: wgpu_tex.texture(),
243                            mip_level: 0,
244                            origin: wgpu::Origin3d {
245                                x: rect.x as u32,
246                                y: rect.y as u32,
247                                z: 0,
248                            },
249                            aspect: wgpu::TextureAspect::All,
250                        },
251                        &padded,
252                        wgpu::TexelCopyBufferLayout {
253                            offset: 0,
254                            bytes_per_row: Some(padded_bytes_per_row),
255                            rows_per_image: Some(height),
256                        },
257                        wgpu::Extent3d {
258                            width,
259                            height,
260                            depth_or_array_layers: 1,
261                        },
262                    );
263                }
264                if cfg!(debug_assertions) {
265                    tracing::debug!(
266                        target: "dear-imgui-wgpu",
267                        "[dear-imgui-wgpu][debug] Updated texture id={} subrect x={} y={} w={} h={}",
268                        texture_id, rect.x, rect.y, rect.w, rect.h
269                    );
270                }
271            } else {
272                // No pixels available, cannot update this rect
273                if cfg!(debug_assertions) {
274                    tracing::debug!(
275                        target: "dear-imgui-wgpu",
276                        "[dear-imgui-wgpu][debug] Skipped subrect update: no pixels available"
277                    );
278                }
279                return Ok(false);
280            }
281        }
282
283        Ok(true)
284    }
285    /// Create a new texture manager
286    pub fn new() -> Self {
287        Self {
288            textures: HashMap::new(),
289            next_id: 1, // Start from 1, 0 is reserved for null texture
290            custom_samplers: HashMap::new(),
291            custom_sampler_by_texture: HashMap::new(),
292            common_bind_groups: HashMap::new(),
293            next_sampler_id: 1, // Start from 1, 0 means "default sampler"
294        }
295    }
296
297    /// Register a new texture and return its ID
298    pub fn register_texture(&mut self, texture: WgpuTexture) -> u64 {
299        let id = self.next_id;
300        self.next_id += 1;
301        self.textures.insert(id, texture);
302        id
303    }
304
305    /// Get a texture by ID
306    pub fn get_texture(&self, id: u64) -> Option<&WgpuTexture> {
307        self.textures.get(&id)
308    }
309
310    /// Remove a texture by ID
311    pub fn remove_texture(&mut self, id: u64) -> Option<WgpuTexture> {
312        self.textures.remove(&id)
313    }
314
315    /// Check if a texture exists
316    pub fn contains_texture(&self, id: u64) -> bool {
317        self.textures.contains_key(&id)
318    }
319
320    /// Insert a texture with a specific ID
321    pub fn insert_texture_with_id(&mut self, id: u64, texture: WgpuTexture) {
322        self.textures.insert(id, texture);
323        // Update next_id if necessary
324        if id >= self.next_id {
325            self.next_id = id + 1;
326        }
327    }
328
329    /// Associate a custom sampler with a texture id (used by external textures).
330    ///
331    /// Returns the internal sampler_id assigned to this sampler.
332    pub(crate) fn set_custom_sampler_for_texture(
333        &mut self,
334        texture_id: u64,
335        sampler: Sampler,
336    ) -> u64 {
337        let sampler_id = self.next_sampler_id;
338        self.next_sampler_id += 1;
339        self.custom_samplers.insert(sampler_id, sampler);
340        self.custom_sampler_by_texture
341            .insert(texture_id, sampler_id);
342        // Invalidate any cached common bind group for this sampler id (defensive).
343        self.common_bind_groups.remove(&sampler_id);
344        sampler_id
345    }
346
347    /// Update or set a custom sampler for an existing texture.
348    ///
349    /// If the texture already has a custom sampler association, we replace the sampler
350    /// in place (keeping the sampler_id stable) and invalidate the cached common bind group.
351    /// If there is no association yet, we create one.
352    ///
353    /// Returns false if the texture_id is not registered.
354    pub(crate) fn update_custom_sampler_for_texture(
355        &mut self,
356        texture_id: u64,
357        sampler: Sampler,
358    ) -> bool {
359        if !self.textures.contains_key(&texture_id) {
360            return false;
361        }
362        if let Some(sampler_id) = self.custom_sampler_by_texture.get(&texture_id).copied() {
363            self.custom_samplers.insert(sampler_id, sampler);
364            self.common_bind_groups.remove(&sampler_id);
365        } else {
366            self.set_custom_sampler_for_texture(texture_id, sampler);
367        }
368        true
369    }
370
371    /// Get the custom sampler id for a texture (if any).
372    pub(crate) fn custom_sampler_id_for_texture(&self, texture_id: u64) -> Option<u64> {
373        self.custom_sampler_by_texture.get(&texture_id).copied()
374    }
375
376    /// Remove any custom sampler association for a texture.
377    pub(crate) fn clear_custom_sampler_for_texture(&mut self, texture_id: u64) {
378        if let Some(sampler_id) = self.custom_sampler_by_texture.remove(&texture_id) {
379            // Drop cached bind group so next use rebuilds it.
380            self.common_bind_groups.remove(&sampler_id);
381        }
382    }
383
384    /// Get or create a common bind group (uniform buffer + sampler) for the given sampler id.
385    ///
386    /// The bind group uses the same uniform buffer but swaps the sampler, allowing
387    /// per-texture sampling without changing the pipeline layout.
388    pub(crate) fn get_or_create_common_bind_group_for_sampler(
389        &mut self,
390        device: &Device,
391        common_layout: &BindGroupLayout,
392        uniform_buffer: &Buffer,
393        sampler_id: u64,
394    ) -> Option<BindGroup> {
395        if let Some(bg) = self.common_bind_groups.get(&sampler_id) {
396            return Some(bg.clone());
397        }
398        let sampler = self.custom_samplers.get(&sampler_id)?;
399        let bg = device.create_bind_group(&BindGroupDescriptor {
400            label: Some("Dear ImGui Common Bind Group (custom sampler)"),
401            layout: common_layout,
402            entries: &[
403                BindGroupEntry {
404                    binding: 0,
405                    resource: uniform_buffer.as_entire_binding(),
406                },
407                BindGroupEntry {
408                    binding: 1,
409                    resource: BindingResource::Sampler(sampler),
410                },
411            ],
412        });
413        self.common_bind_groups.insert(sampler_id, bg.clone());
414        Some(bg)
415    }
416
417    /// Destroy a texture by ID
418    pub fn destroy_texture_by_id(&mut self, id: u64) {
419        self.remove_texture(id);
420    }
421
422    /// Update an existing texture from Dear ImGui texture data with specific ID
423    pub fn update_texture_from_data_with_id(
424        &mut self,
425        device: &Device,
426        queue: &Queue,
427        texture_data: &TextureData,
428        texture_id: u64,
429    ) -> RendererResult<()> {
430        // For WGPU, we recreate the texture instead of updating in place
431        // This is simpler and more reliable than trying to update existing textures
432        if self.contains_texture(texture_id) {
433            // Remove old texture
434            self.remove_texture(texture_id);
435
436            // Create new texture
437            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
438
439            // Move the texture to the correct ID slot if needed
440            if new_texture_id != texture_id
441                && let Some(texture) = self.remove_texture(new_texture_id)
442            {
443                self.insert_texture_with_id(texture_id, texture);
444            }
445
446            Ok(())
447        } else {
448            Err(RendererError::InvalidTextureId(texture_id))
449        }
450    }
451
452    /// Get the number of registered textures
453    pub fn texture_count(&self) -> usize {
454        self.textures.len()
455    }
456
457    /// Clear all textures
458    pub fn clear(&mut self) {
459        self.textures.clear();
460        self.next_id = 1;
461        self.custom_sampler_by_texture.clear();
462        self.common_bind_groups.clear();
463        // Keep samplers around? Clear to avoid holding stale handles after device loss.
464        self.custom_samplers.clear();
465        self.next_sampler_id = 1;
466    }
467}
468
469/// Texture creation and management functions
470impl WgpuTextureManager {
471    /// Create a texture from Dear ImGui texture data
472    pub fn create_texture_from_data(
473        &mut self,
474        device: &Device,
475        queue: &Queue,
476        texture_data: &TextureData,
477    ) -> RendererResult<u64> {
478        let width = texture_data.width() as u32;
479        let height = texture_data.height() as u32;
480        let format = texture_data.format();
481
482        let pixels = texture_data
483            .pixels()
484            .ok_or_else(|| RendererError::BadTexture("No pixel data available".to_string()))?;
485
486        // Convert ImGui texture format to WGPU format and handle data conversion
487        // This matches the texture format handling in imgui_impl_wgpu.cpp
488        let (wgpu_format, converted_data, _bytes_per_pixel) = match format {
489            ImGuiTextureFormat::RGBA32 => {
490                // RGBA32 maps directly to RGBA8Unorm (matches C++ implementation)
491                if pixels.len() != (width * height * 4) as usize {
492                    return Err(RendererError::BadTexture(format!(
493                        "RGBA32 texture data size mismatch: expected {} bytes, got {}",
494                        width * height * 4,
495                        pixels.len()
496                    )));
497                }
498                (TextureFormat::Rgba8Unorm, pixels.to_vec(), 4u32)
499            }
500            ImGuiTextureFormat::Alpha8 => {
501                // Convert Alpha8 to RGBA32 for WGPU (white RGB + original alpha)
502                // This ensures compatibility with the standard RGBA8Unorm format
503                if pixels.len() != (width * height) as usize {
504                    return Err(RendererError::BadTexture(format!(
505                        "Alpha8 texture data size mismatch: expected {} bytes, got {}",
506                        width * height,
507                        pixels.len()
508                    )));
509                }
510                let mut rgba_data = Vec::with_capacity(pixels.len() * 4);
511                for &alpha in pixels {
512                    rgba_data.extend_from_slice(&[255, 255, 255, alpha]); // White RGB + alpha
513                }
514                (TextureFormat::Rgba8Unorm, rgba_data, 4u32)
515            }
516        };
517
518        // Create WGPU texture (matches the descriptor setup in imgui_impl_wgpu.cpp)
519        if cfg!(debug_assertions) {
520            tracing::debug!(
521                target: "dear-imgui-wgpu",
522                "[dear-imgui-wgpu][debug] Create texture: {}x{} format={:?}",
523                width, height, format
524            );
525        }
526        let texture = device.create_texture(&TextureDescriptor {
527            label: Some("Dear ImGui Texture"),
528            size: Extent3d {
529                width,
530                height,
531                depth_or_array_layers: 1,
532            },
533            mip_level_count: 1,
534            sample_count: 1,
535            dimension: TextureDimension::D2,
536            format: wgpu_format,
537            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
538            view_formats: &[],
539        });
540
541        // Validate texture data size before upload
542        let expected_size = (width * height * 4) as usize; // Always RGBA after conversion
543        if converted_data.len() != expected_size {
544            return Err(RendererError::BadTexture(format!(
545                "Converted texture data size mismatch: expected {} bytes, got {}",
546                expected_size,
547                converted_data.len()
548            )));
549        }
550
551        // Upload texture data (matches the upload logic in imgui_impl_wgpu.cpp)
552        // WebGPU requires bytes_per_row to be 256-byte aligned. Pad rows if needed.
553        let bpp = 4u32;
554        let unpadded_bytes_per_row = width * bpp;
555        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
556        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
557        if padded_bytes_per_row == unpadded_bytes_per_row {
558            // Aligned: direct upload
559            queue.write_texture(
560                wgpu::TexelCopyTextureInfo {
561                    texture: &texture,
562                    mip_level: 0,
563                    origin: wgpu::Origin3d::ZERO,
564                    aspect: wgpu::TextureAspect::All,
565                },
566                &converted_data,
567                wgpu::TexelCopyBufferLayout {
568                    offset: 0,
569                    bytes_per_row: Some(unpadded_bytes_per_row),
570                    rows_per_image: Some(height),
571                },
572                Extent3d {
573                    width,
574                    height,
575                    depth_or_array_layers: 1,
576                },
577            );
578        } else {
579            // Pad each row to the required alignment
580            let mut padded: Vec<u8> = vec![0; (padded_bytes_per_row * height) as usize];
581            for row in 0..height as usize {
582                let src_off = row * (unpadded_bytes_per_row as usize);
583                let dst_off = row * (padded_bytes_per_row as usize);
584                padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)].copy_from_slice(
585                    &converted_data[src_off..src_off + (unpadded_bytes_per_row as usize)],
586                );
587            }
588            queue.write_texture(
589                wgpu::TexelCopyTextureInfo {
590                    texture: &texture,
591                    mip_level: 0,
592                    origin: wgpu::Origin3d::ZERO,
593                    aspect: wgpu::TextureAspect::All,
594                },
595                &padded,
596                wgpu::TexelCopyBufferLayout {
597                    offset: 0,
598                    bytes_per_row: Some(padded_bytes_per_row),
599                    rows_per_image: Some(height),
600                },
601                Extent3d {
602                    width,
603                    height,
604                    depth_or_array_layers: 1,
605                },
606            );
607            if cfg!(debug_assertions) {
608                tracing::debug!(
609                    target: "dear-imgui-wgpu",
610                    "[dear-imgui-wgpu][debug] Upload texture with padded row pitch: unpadded={} padded={}",
611                    unpadded_bytes_per_row, padded_bytes_per_row
612                );
613            }
614        }
615
616        // Create texture view
617        let texture_view = texture.create_view(&TextureViewDescriptor::default());
618
619        // Create WGPU texture wrapper
620        let wgpu_texture = WgpuTexture::new(texture, texture_view);
621
622        // Register and return ID
623        let texture_id = self.register_texture(wgpu_texture);
624        if cfg!(debug_assertions) {
625            tracing::debug!(
626                target: "dear-imgui-wgpu",
627                "[dear-imgui-wgpu][debug] Texture registered: id={}",
628                texture_id
629            );
630        }
631        Ok(texture_id)
632    }
633
634    /// Update an existing texture from Dear ImGui texture data
635    pub fn update_texture_from_data(
636        &mut self,
637        device: &Device,
638        queue: &Queue,
639        texture_data: &TextureData,
640    ) -> RendererResult<()> {
641        let texture_id = texture_data.tex_id().id();
642
643        // If the texture already exists and the TextureData only requests sub-rectangle
644        // updates, honor them in-place to match Dear ImGui 1.92 semantics.
645        // Fallback to full re-create when there is no pixel data available.
646        if self.contains_texture(texture_id) {
647            // Attempt sub-rect updates first (preferred path)
648            if self.apply_subrect_updates(queue, texture_data, texture_id)? {
649                return Ok(());
650            }
651
652            // Otherwise, recreate from full data
653            self.remove_texture(texture_id);
654            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
655            if new_texture_id != texture_id
656                && let Some(texture) = self.remove_texture(new_texture_id)
657            {
658                self.insert_texture_with_id(texture_id, texture);
659            }
660        } else {
661            // Create new texture if it doesn't exist
662            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
663            if new_texture_id != texture_id
664                && let Some(texture) = self.remove_texture(new_texture_id)
665            {
666                self.insert_texture_with_id(texture_id, texture);
667            }
668        }
669
670        Ok(())
671    }
672
673    /// Destroy a texture
674    pub fn destroy_texture(&mut self, texture_id: TextureId) {
675        let texture_id_u64 = texture_id.id();
676        self.remove_texture(texture_id_u64);
677        // WGPU textures are automatically cleaned up when dropped
678    }
679
680    /// Handle texture updates from Dear ImGui draw data
681    ///
682    /// This iterates `DrawData::textures()` and applies create/update/destroy requests.
683    /// For `WantCreate`, we create the GPU texture, then write the generated id back into
684    /// the `ImTextureData` via `set_tex_id()` and mark status `OK` (matching C++ backend).
685    /// For `WantUpdates`, if a valid id is not yet assigned (first use), we create now and
686    /// assign the id; otherwise we update in place. When textures are recreated or destroyed,
687    /// the corresponding cached bind groups in `RenderResources` are invalidated so that
688    /// subsequent draws will see the updated views.
689    pub fn handle_texture_updates(
690        &mut self,
691        draw_data: &dear_imgui_rs::render::DrawData,
692        device: &Device,
693        queue: &Queue,
694        render_resources: &mut RenderResources,
695    ) {
696        for mut texture_data in draw_data.textures() {
697            let status = texture_data.status();
698            let current_tex_id = texture_data.tex_id().id();
699
700            match status {
701                TextureStatus::WantCreate => {
702                    // Create and upload new texture to graphics system
703                    // Following the official imgui_impl_wgpu.cpp implementation
704
705                    // If ImGui already had a TexID associated, drop any stale bind group
706                    // so that a new one is created the first time we render with it.
707                    if current_tex_id != 0 {
708                        render_resources.remove_image_bind_group(current_tex_id);
709                    }
710
711                    match self.create_texture_from_data(device, queue, &*texture_data) {
712                        Ok(wgpu_texture_id) => {
713                            // CRITICAL: Set the texture ID back to Dear ImGui
714                            // In the C++ implementation, they use the TextureView pointer as ImTextureID.
715                            // In Rust, we can't get the raw pointer, so we use our internal texture ID.
716                            // This works because our renderer will map the texture ID to the WGPU texture.
717                            let new_texture_id = dear_imgui_rs::TextureId::from(wgpu_texture_id);
718
719                            texture_data.set_tex_id(new_texture_id);
720
721                            // Mark texture as ready
722                            texture_data.set_status(TextureStatus::OK);
723                        }
724                        Err(e) => {
725                            println!(
726                                "Failed to create texture for ID: {}, error: {}",
727                                current_tex_id, e
728                            );
729                        }
730                    }
731                }
732                TextureStatus::WantUpdates => {
733                    let imgui_tex_id = texture_data.tex_id();
734                    let internal_id = imgui_tex_id.id();
735
736                    // If we don't have a valid texture id yet (first update) or the
737                    // id isn't registered, create it now and write back the TexID,
738                    // so this frame (or the next one) can bind the correct texture.
739                    if internal_id == 0 || !self.contains_texture(internal_id) {
740                        match self.create_texture_from_data(device, queue, &*texture_data) {
741                            Ok(new_id) => {
742                                texture_data.set_tex_id(dear_imgui_rs::TextureId::from(new_id));
743                                texture_data.set_status(TextureStatus::OK);
744                            }
745                            Err(_e) => {
746                                // Leave it destroyed to avoid retry storm; user can request create again
747                                texture_data.set_status(TextureStatus::Destroyed);
748                            }
749                        }
750                    } else {
751                        // We are about to update/recreate an existing texture. Invalidate
752                        // any cached bind group so it will be rebuilt with the new view.
753                        render_resources.remove_image_bind_group(internal_id);
754
755                        // Try in-place sub-rect updates first
756                        if self
757                            .apply_subrect_updates(queue, &*texture_data, internal_id)
758                            .unwrap_or(false)
759                        {
760                            texture_data.set_status(TextureStatus::OK);
761                        } else if self
762                            .update_texture_from_data_with_id(
763                                device,
764                                queue,
765                                &*texture_data,
766                                internal_id,
767                            )
768                            .is_err()
769                        {
770                            // If update fails, keep the existing GPU texture and mark OK to avoid a retry storm.
771                            // We cannot clear TexID here because draw commands in this frame may still reference it.
772                            texture_data.set_status(TextureStatus::OK);
773                        } else {
774                            texture_data.set_status(TextureStatus::OK);
775                        }
776                    }
777                }
778                TextureStatus::WantDestroy => {
779                    // Only destroy when unused frames > 0 (align with official backend behavior)
780                    let mut can_destroy = true;
781                    unsafe {
782                        let raw = texture_data.as_raw();
783                        if !raw.is_null() {
784                            // If field not present in bindings on some versions, default true
785                            #[allow(unused_unsafe)]
786                            {
787                                // Access UnusedFrames if available
788                                // SAFETY: reading a plain field from raw C struct
789                                can_destroy = (*raw).UnusedFrames > 0;
790                            }
791                        }
792                    }
793                    if can_destroy {
794                        let imgui_tex_id = texture_data.tex_id();
795                        let internal_id = imgui_tex_id.id();
796                        // Remove from texture cache and any associated bind groups
797                        self.remove_texture(internal_id);
798                        self.clear_custom_sampler_for_texture(internal_id);
799                        render_resources.remove_image_bind_group(internal_id);
800                        texture_data.set_status(TextureStatus::Destroyed);
801                    }
802                }
803                TextureStatus::OK | TextureStatus::Destroyed => {
804                    // No action needed
805                }
806            }
807        }
808    }
809
810    /// Update a single texture based on its status
811    ///
812    /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
813    ///
814    /// # Returns
815    ///
816    /// Returns a `TextureUpdateResult` that contains the operation result and
817    /// any status/ID updates that need to be applied to the texture data.
818    /// This follows Rust's principle of explicit state management.
819    ///
820    /// # Example
821    ///
822    /// ```rust,no_run
823    /// # use dear_imgui_wgpu::*;
824    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
825    /// # let mut texture_manager = WgpuTextureManager::new();
826    /// # let device = todo!();
827    /// # let queue = todo!();
828    /// # let mut texture_data = dear_imgui_rs::TextureData::new();
829    /// let result = texture_manager.update_single_texture(&texture_data, &device, &queue)?;
830    /// result.apply_to(&mut texture_data);
831    /// # Ok(())
832    /// # }
833    /// ```
834    pub fn update_single_texture(
835        &mut self,
836        texture_data: &dear_imgui_rs::TextureData,
837        device: &Device,
838        queue: &Queue,
839    ) -> RendererResult<TextureUpdateResult> {
840        match texture_data.status() {
841            TextureStatus::WantCreate => {
842                let texture_id = self.create_texture_from_data(device, queue, texture_data)?;
843                Ok(TextureUpdateResult::Created {
844                    texture_id: TextureId::from(texture_id),
845                })
846            }
847            TextureStatus::WantUpdates => {
848                let internal_id = texture_data.tex_id().id();
849                if internal_id == 0 || !self.contains_texture(internal_id) {
850                    // No valid ID yet: create now and return Created so caller can set TexID
851                    let texture_id = self.create_texture_from_data(device, queue, texture_data)?;
852                    Ok(TextureUpdateResult::Created {
853                        texture_id: TextureId::from(texture_id),
854                    })
855                } else {
856                    match self.update_texture_from_data_with_id(
857                        device,
858                        queue,
859                        texture_data,
860                        internal_id,
861                    ) {
862                        Ok(_) => Ok(TextureUpdateResult::Updated),
863                        Err(e) => Err(e),
864                    }
865                }
866            }
867            TextureStatus::WantDestroy => {
868                self.destroy_texture(texture_data.tex_id());
869                Ok(TextureUpdateResult::Destroyed)
870            }
871            TextureStatus::OK | TextureStatus::Destroyed => Ok(TextureUpdateResult::NoAction),
872        }
873    }
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879    use dear_imgui_rs::texture::{TextureData, TextureFormat as ImFormat, TextureRect};
880
881    #[test]
882    fn texture_update_result_apply_to_sets_status_and_id() {
883        let mut tex = TextureData::new();
884
885        // Created -> sets TexID and OK
886        TextureUpdateResult::Created {
887            texture_id: TextureId::from(42u64),
888        }
889        .apply_to(&mut tex);
890        assert_eq!(tex.status(), TextureStatus::OK);
891        assert_eq!(tex.tex_id().id(), 42);
892
893        // Updated -> only status OK
894        TextureUpdateResult::Updated.apply_to(&mut tex);
895        assert_eq!(tex.status(), TextureStatus::OK);
896        assert_eq!(tex.tex_id().id(), 42);
897
898        // Destroyed -> status Destroyed
899        // ImGui's ImTextureData::SetStatus has special semantics:
900        // setting Destroyed while WantDestroyNextFrame is false will immediately flip back to WantCreate.
901        // When honoring a requested destroy, WantDestroyNextFrame is expected to be true.
902        unsafe {
903            (*tex.as_raw_mut()).WantDestroyNextFrame = true;
904        }
905        TextureUpdateResult::Destroyed.apply_to(&mut tex);
906        assert_eq!(tex.status(), TextureStatus::Destroyed);
907
908        // Failed -> also marks Destroyed
909        // In the general case (not a requested destroy), SetStatus(Destroyed) translates to WantCreate.
910        unsafe {
911            (*tex.as_raw_mut()).WantDestroyNextFrame = false;
912        }
913        tex.create(ImFormat::RGBA32, 1, 1);
914        TextureUpdateResult::Failed.apply_to(&mut tex);
915        assert_eq!(tex.status(), TextureStatus::WantCreate);
916
917        // NoAction -> leaves state unchanged
918        TextureUpdateResult::NoAction.apply_to(&mut tex);
919        assert_eq!(tex.status(), TextureStatus::WantCreate);
920    }
921
922    #[test]
923    fn convert_subrect_to_rgba_rgba32_full_rect() {
924        let mut tex = TextureData::new();
925        let width = 2;
926        let height = 2;
927        tex.create(ImFormat::RGBA32, width, height);
928
929        // 2x2 RGBA pixels: row-major
930        let pixels: [u8; 16] = [
931            10, 20, 30, 40, // (0,0)
932            50, 60, 70, 80, // (1,0)
933            90, 100, 110, 120, // (0,1)
934            130, 140, 150, 160, // (1,1)
935        ];
936        tex.set_data(&pixels);
937
938        let rect = TextureRect {
939            x: 0,
940            y: 0,
941            w: width as u16,
942            h: height as u16,
943        };
944
945        let out = WgpuTextureManager::convert_subrect_to_rgba(&tex, rect).expect("expected data");
946        assert_eq!(out, pixels);
947    }
948
949    #[test]
950    fn convert_subrect_to_rgba_alpha8_full_rect() {
951        let mut tex = TextureData::new();
952        let width = 2;
953        let height = 2;
954        tex.create(ImFormat::Alpha8, width, height);
955
956        // 2x2 alpha-only pixels
957        let alphas: [u8; 4] = [0, 64, 128, 255];
958        tex.set_data(&alphas);
959
960        let rect = TextureRect {
961            x: 0,
962            y: 0,
963            w: width as u16,
964            h: height as u16,
965        };
966
967        let out = WgpuTextureManager::convert_subrect_to_rgba(&tex, rect).expect("expected data");
968        // Each alpha should expand to [255,255,255,a]
969        assert_eq!(
970            out,
971            vec![
972                255, 255, 255, 0, // a=0
973                255, 255, 255, 64, // a=64
974                255, 255, 255, 128, // a=128
975                255, 255, 255, 255, // a=255
976            ]
977        );
978    }
979
980    #[test]
981    fn convert_subrect_to_rgba_out_of_bounds_returns_none() {
982        let mut tex = TextureData::new();
983        tex.create(ImFormat::RGBA32, 2, 2);
984        let rect = TextureRect {
985            x: 10,
986            y: 10,
987            w: 1,
988            h: 1,
989        };
990        assert!(WgpuTextureManager::convert_subrect_to_rgba(&tex, rect).is_none());
991    }
992}