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_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, 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    /// Convert a sub-rectangle of ImGui texture pixels into a tightly packed RGBA8 buffer
102    fn convert_subrect_to_rgba(
103        texture_data: &TextureData,
104        rect: dear_imgui_rs::texture::TextureRect,
105    ) -> Option<Vec<u8>> {
106        let pixels = texture_data.pixels()?;
107        let tex_w = texture_data.width() as usize;
108        let tex_h = texture_data.height() as usize;
109        if tex_w == 0 || tex_h == 0 {
110            return None;
111        }
112
113        let bpp = texture_data.bytes_per_pixel() as usize;
114        let (rx, ry, rw, rh) = (
115            rect.x as usize,
116            rect.y as usize,
117            rect.w as usize,
118            rect.h as usize,
119        );
120        if rw == 0 || rh == 0 || rx >= tex_w || ry >= tex_h {
121            return None;
122        }
123
124        // Clamp to texture bounds defensively
125        let rw = rw.min(tex_w.saturating_sub(rx));
126        let rh = rh.min(tex_h.saturating_sub(ry));
127
128        let mut out = vec![0u8; rw * rh * 4];
129        match texture_data.format() {
130            ImGuiTextureFormat::RGBA32 => {
131                for row in 0..rh {
132                    let src_off = ((ry + row) * tex_w + rx) * bpp;
133                    let dst_off = row * rw * 4;
134                    // Copy only the row slice and convert layout if needed (it is already RGBA)
135                    out[dst_off..dst_off + rw * 4]
136                        .copy_from_slice(&pixels[src_off..src_off + rw * 4]);
137                }
138            }
139            ImGuiTextureFormat::Alpha8 => {
140                for row in 0..rh {
141                    let src_off = ((ry + row) * tex_w + rx) * bpp; // bpp = 1
142                    let dst_off = row * rw * 4;
143                    for i in 0..rw {
144                        let a = pixels[src_off + i];
145                        let dst = &mut out[dst_off + i * 4..dst_off + i * 4 + 4];
146                        dst.copy_from_slice(&[255, 255, 255, a]);
147                    }
148                }
149            }
150        }
151        Some(out)
152    }
153
154    /// Apply queued sub-rectangle updates to an existing WGPU texture.
155    /// Returns true if any update was applied.
156    fn apply_subrect_updates(
157        &mut self,
158        queue: &Queue,
159        texture_data: &TextureData,
160        texture_id: u64,
161    ) -> RendererResult<bool> {
162        let wgpu_tex = match self.textures.get(&texture_id) {
163            Some(t) => t,
164            None => return Ok(false),
165        };
166
167        // Collect update rectangles; prefer explicit Updates[] if present,
168        // otherwise fallback to single UpdateRect.
169        let mut rects: Vec<dear_imgui_rs::texture::TextureRect> = texture_data.updates().collect();
170        if rects.is_empty() {
171            let r = texture_data.update_rect();
172            if r.w > 0 && r.h > 0 {
173                rects.push(r);
174            }
175        }
176        if rects.is_empty() {
177            return Ok(false);
178        }
179
180        // Upload each rect
181        for rect in rects {
182            if let Some(tight_rgba) = Self::convert_subrect_to_rgba(texture_data, rect) {
183                let width = rect.w as u32;
184                let height = rect.h as u32;
185                let bpp = 4u32;
186                let unpadded_bytes_per_row = width * bpp;
187                let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
188                let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
189
190                if padded_bytes_per_row == unpadded_bytes_per_row {
191                    // Aligned: direct upload
192                    queue.write_texture(
193                        wgpu::TexelCopyTextureInfo {
194                            texture: wgpu_tex.texture(),
195                            mip_level: 0,
196                            origin: wgpu::Origin3d {
197                                x: rect.x as u32,
198                                y: rect.y as u32,
199                                z: 0,
200                            },
201                            aspect: wgpu::TextureAspect::All,
202                        },
203                        &tight_rgba,
204                        wgpu::TexelCopyBufferLayout {
205                            offset: 0,
206                            bytes_per_row: Some(unpadded_bytes_per_row),
207                            rows_per_image: Some(height),
208                        },
209                        wgpu::Extent3d {
210                            width,
211                            height,
212                            depth_or_array_layers: 1,
213                        },
214                    );
215                } else {
216                    // Pad each row to the required alignment
217                    let mut padded = vec![0u8; (padded_bytes_per_row * height) as usize];
218                    for row in 0..height as usize {
219                        let src_off = row * (unpadded_bytes_per_row as usize);
220                        let dst_off = row * (padded_bytes_per_row as usize);
221                        padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)]
222                            .copy_from_slice(
223                                &tight_rgba[src_off..src_off + (unpadded_bytes_per_row as usize)],
224                            );
225                    }
226                    queue.write_texture(
227                        wgpu::TexelCopyTextureInfo {
228                            texture: wgpu_tex.texture(),
229                            mip_level: 0,
230                            origin: wgpu::Origin3d {
231                                x: rect.x as u32,
232                                y: rect.y as u32,
233                                z: 0,
234                            },
235                            aspect: wgpu::TextureAspect::All,
236                        },
237                        &padded,
238                        wgpu::TexelCopyBufferLayout {
239                            offset: 0,
240                            bytes_per_row: Some(padded_bytes_per_row),
241                            rows_per_image: Some(height),
242                        },
243                        wgpu::Extent3d {
244                            width,
245                            height,
246                            depth_or_array_layers: 1,
247                        },
248                    );
249                }
250                if cfg!(debug_assertions) {
251                    tracing::debug!(
252                        target: "dear-imgui-wgpu",
253                        "[dear-imgui-wgpu][debug] Updated texture id={} subrect x={} y={} w={} h={}",
254                        texture_id, rect.x, rect.y, rect.w, rect.h
255                    );
256                }
257            } else {
258                // No pixels available, cannot update this rect
259                if cfg!(debug_assertions) {
260                    tracing::debug!(
261                        target: "dear-imgui-wgpu",
262                        "[dear-imgui-wgpu][debug] Skipped subrect update: no pixels available"
263                    );
264                }
265                return Ok(false);
266            }
267        }
268
269        Ok(true)
270    }
271    /// Create a new texture manager
272    pub fn new() -> Self {
273        Self {
274            textures: HashMap::new(),
275            next_id: 1, // Start from 1, 0 is reserved for null texture
276        }
277    }
278
279    /// Register a new texture and return its ID
280    pub fn register_texture(&mut self, texture: WgpuTexture) -> u64 {
281        let id = self.next_id;
282        self.next_id += 1;
283        self.textures.insert(id, texture);
284        id
285    }
286
287    /// Get a texture by ID
288    pub fn get_texture(&self, id: u64) -> Option<&WgpuTexture> {
289        self.textures.get(&id)
290    }
291
292    /// Remove a texture by ID
293    pub fn remove_texture(&mut self, id: u64) -> Option<WgpuTexture> {
294        self.textures.remove(&id)
295    }
296
297    /// Check if a texture exists
298    pub fn contains_texture(&self, id: u64) -> bool {
299        self.textures.contains_key(&id)
300    }
301
302    /// Insert a texture with a specific ID
303    pub fn insert_texture_with_id(&mut self, id: u64, texture: WgpuTexture) {
304        self.textures.insert(id, texture);
305        // Update next_id if necessary
306        if id >= self.next_id {
307            self.next_id = id + 1;
308        }
309    }
310
311    /// Destroy a texture by ID
312    pub fn destroy_texture_by_id(&mut self, id: u64) {
313        self.remove_texture(id);
314    }
315
316    /// Update an existing texture from Dear ImGui texture data with specific ID
317    pub fn update_texture_from_data_with_id(
318        &mut self,
319        device: &Device,
320        queue: &Queue,
321        texture_data: &TextureData,
322        texture_id: u64,
323    ) -> RendererResult<()> {
324        // For WGPU, we recreate the texture instead of updating in place
325        // This is simpler and more reliable than trying to update existing textures
326        if self.contains_texture(texture_id) {
327            // Remove old texture
328            self.remove_texture(texture_id);
329
330            // Create new texture
331            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
332
333            // Move the texture to the correct ID slot if needed
334            if new_texture_id != texture_id
335                && let Some(texture) = self.remove_texture(new_texture_id)
336            {
337                self.insert_texture_with_id(texture_id, texture);
338            }
339
340            Ok(())
341        } else {
342            Err(RendererError::InvalidTextureId(texture_id))
343        }
344    }
345
346    /// Get the number of registered textures
347    pub fn texture_count(&self) -> usize {
348        self.textures.len()
349    }
350
351    /// Clear all textures
352    pub fn clear(&mut self) {
353        self.textures.clear();
354        self.next_id = 1;
355    }
356}
357
358/// Texture creation and management functions
359impl WgpuTextureManager {
360    /// Create a texture from Dear ImGui texture data
361    pub fn create_texture_from_data(
362        &mut self,
363        device: &Device,
364        queue: &Queue,
365        texture_data: &TextureData,
366    ) -> RendererResult<u64> {
367        let width = texture_data.width() as u32;
368        let height = texture_data.height() as u32;
369        let format = texture_data.format();
370
371        let pixels = texture_data
372            .pixels()
373            .ok_or_else(|| RendererError::BadTexture("No pixel data available".to_string()))?;
374
375        // Convert ImGui texture format to WGPU format and handle data conversion
376        // This matches the texture format handling in imgui_impl_wgpu.cpp
377        let (wgpu_format, converted_data, _bytes_per_pixel) = match format {
378            ImGuiTextureFormat::RGBA32 => {
379                // RGBA32 maps directly to RGBA8Unorm (matches C++ implementation)
380                if pixels.len() != (width * height * 4) as usize {
381                    return Err(RendererError::BadTexture(format!(
382                        "RGBA32 texture data size mismatch: expected {} bytes, got {}",
383                        width * height * 4,
384                        pixels.len()
385                    )));
386                }
387                (TextureFormat::Rgba8Unorm, pixels.to_vec(), 4u32)
388            }
389            ImGuiTextureFormat::Alpha8 => {
390                // Convert Alpha8 to RGBA32 for WGPU (white RGB + original alpha)
391                // This ensures compatibility with the standard RGBA8Unorm format
392                if pixels.len() != (width * height) as usize {
393                    return Err(RendererError::BadTexture(format!(
394                        "Alpha8 texture data size mismatch: expected {} bytes, got {}",
395                        width * height,
396                        pixels.len()
397                    )));
398                }
399                let mut rgba_data = Vec::with_capacity(pixels.len() * 4);
400                for &alpha in pixels {
401                    rgba_data.extend_from_slice(&[255, 255, 255, alpha]); // White RGB + alpha
402                }
403                (TextureFormat::Rgba8Unorm, rgba_data, 4u32)
404            }
405        };
406
407        // Create WGPU texture (matches the descriptor setup in imgui_impl_wgpu.cpp)
408        if cfg!(debug_assertions) {
409            tracing::debug!(
410                target: "dear-imgui-wgpu",
411                "[dear-imgui-wgpu][debug] Create texture: {}x{} format={:?}",
412                width, height, format
413            );
414        }
415        let texture = device.create_texture(&TextureDescriptor {
416            label: Some("Dear ImGui Texture"),
417            size: Extent3d {
418                width,
419                height,
420                depth_or_array_layers: 1,
421            },
422            mip_level_count: 1,
423            sample_count: 1,
424            dimension: TextureDimension::D2,
425            format: wgpu_format,
426            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
427            view_formats: &[],
428        });
429
430        // Validate texture data size before upload
431        let expected_size = (width * height * 4) as usize; // Always RGBA after conversion
432        if converted_data.len() != expected_size {
433            return Err(RendererError::BadTexture(format!(
434                "Converted texture data size mismatch: expected {} bytes, got {}",
435                expected_size,
436                converted_data.len()
437            )));
438        }
439
440        // Upload texture data (matches the upload logic in imgui_impl_wgpu.cpp)
441        // WebGPU requires bytes_per_row to be 256-byte aligned. Pad rows if needed.
442        let bpp = 4u32;
443        let unpadded_bytes_per_row = width * bpp;
444        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
445        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
446        if padded_bytes_per_row == unpadded_bytes_per_row {
447            // Aligned: direct upload
448            queue.write_texture(
449                wgpu::TexelCopyTextureInfo {
450                    texture: &texture,
451                    mip_level: 0,
452                    origin: wgpu::Origin3d::ZERO,
453                    aspect: wgpu::TextureAspect::All,
454                },
455                &converted_data,
456                wgpu::TexelCopyBufferLayout {
457                    offset: 0,
458                    bytes_per_row: Some(unpadded_bytes_per_row),
459                    rows_per_image: Some(height),
460                },
461                Extent3d {
462                    width,
463                    height,
464                    depth_or_array_layers: 1,
465                },
466            );
467        } else {
468            // Pad each row to the required alignment
469            let mut padded: Vec<u8> = vec![0; (padded_bytes_per_row * height) as usize];
470            for row in 0..height as usize {
471                let src_off = row * (unpadded_bytes_per_row as usize);
472                let dst_off = row * (padded_bytes_per_row as usize);
473                padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)].copy_from_slice(
474                    &converted_data[src_off..src_off + (unpadded_bytes_per_row as usize)],
475                );
476            }
477            queue.write_texture(
478                wgpu::TexelCopyTextureInfo {
479                    texture: &texture,
480                    mip_level: 0,
481                    origin: wgpu::Origin3d::ZERO,
482                    aspect: wgpu::TextureAspect::All,
483                },
484                &padded,
485                wgpu::TexelCopyBufferLayout {
486                    offset: 0,
487                    bytes_per_row: Some(padded_bytes_per_row),
488                    rows_per_image: Some(height),
489                },
490                Extent3d {
491                    width,
492                    height,
493                    depth_or_array_layers: 1,
494                },
495            );
496            if cfg!(debug_assertions) {
497                tracing::debug!(
498                    target: "dear-imgui-wgpu",
499                    "[dear-imgui-wgpu][debug] Upload texture with padded row pitch: unpadded={} padded={}",
500                    unpadded_bytes_per_row, padded_bytes_per_row
501                );
502            }
503        }
504
505        // Create texture view
506        let texture_view = texture.create_view(&TextureViewDescriptor::default());
507
508        // Create WGPU texture wrapper
509        let wgpu_texture = WgpuTexture::new(texture, texture_view);
510
511        // Register and return ID
512        let texture_id = self.register_texture(wgpu_texture);
513        if cfg!(debug_assertions) {
514            tracing::debug!(
515                target: "dear-imgui-wgpu",
516                "[dear-imgui-wgpu][debug] Texture registered: id={}",
517                texture_id
518            );
519        }
520        Ok(texture_id)
521    }
522
523    /// Update an existing texture from Dear ImGui texture data
524    pub fn update_texture_from_data(
525        &mut self,
526        device: &Device,
527        queue: &Queue,
528        texture_data: &TextureData,
529    ) -> RendererResult<()> {
530        let texture_id = texture_data.tex_id().id();
531
532        // If the texture already exists and the TextureData only requests sub-rectangle
533        // updates, honor them in-place to match Dear ImGui 1.92 semantics.
534        // Fallback to full re-create when there is no pixel data available.
535        if self.contains_texture(texture_id) {
536            // Attempt sub-rect updates first (preferred path)
537            if self.apply_subrect_updates(queue, texture_data, texture_id)? {
538                return Ok(());
539            }
540
541            // Otherwise, recreate from full data
542            self.remove_texture(texture_id);
543            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
544            if new_texture_id != texture_id
545                && let Some(texture) = self.remove_texture(new_texture_id)
546            {
547                self.insert_texture_with_id(texture_id, texture);
548            }
549        } else {
550            // Create new texture if it doesn't exist
551            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
552            if new_texture_id != texture_id
553                && let Some(texture) = self.remove_texture(new_texture_id)
554            {
555                self.insert_texture_with_id(texture_id, texture);
556            }
557        }
558
559        Ok(())
560    }
561
562    /// Destroy a texture
563    pub fn destroy_texture(&mut self, texture_id: TextureId) {
564        let texture_id_u64 = texture_id.id();
565        self.remove_texture(texture_id_u64);
566        // WGPU textures are automatically cleaned up when dropped
567    }
568
569    /// Handle texture updates from Dear ImGui draw data
570    ///
571    /// This iterates `DrawData::textures()` and applies create/update/destroy requests.
572    /// For `WantCreate`, we create the GPU texture, then write the generated id back into
573    /// the `ImTextureData` via `set_tex_id()` and mark status `OK` (matching C++ backend).
574    /// For `WantUpdates`, if a valid id is not yet assigned (first use), we create now and
575    /// assign the id; otherwise we update in place.
576    pub fn handle_texture_updates(
577        &mut self,
578        draw_data: &dear_imgui_rs::render::DrawData,
579        device: &Device,
580        queue: &Queue,
581    ) {
582        for texture_data in draw_data.textures() {
583            let status = texture_data.status();
584            let current_tex_id = texture_data.tex_id().id();
585
586            match status {
587                TextureStatus::WantCreate => {
588                    // Create and upload new texture to graphics system
589                    // Following the official imgui_impl_wgpu.cpp implementation
590
591                    match self.create_texture_from_data(device, queue, texture_data) {
592                        Ok(wgpu_texture_id) => {
593                            // CRITICAL: Set the texture ID back to Dear ImGui
594                            // In the C++ implementation, they use the TextureView pointer as ImTextureID.
595                            // In Rust, we can't get the raw pointer, so we use our internal texture ID.
596                            // This works because our renderer will map the texture ID to the WGPU texture.
597                            let new_texture_id = dear_imgui_rs::TextureId::from(wgpu_texture_id);
598
599                            texture_data.set_tex_id(new_texture_id);
600
601                            // Mark texture as ready
602                            texture_data.set_status(TextureStatus::OK);
603                        }
604                        Err(e) => {
605                            println!(
606                                "Failed to create texture for ID: {}, error: {}",
607                                current_tex_id, e
608                            );
609                        }
610                    }
611                }
612                TextureStatus::WantUpdates => {
613                    let imgui_tex_id = texture_data.tex_id();
614                    let internal_id = imgui_tex_id.id();
615
616                    // If we don't have a valid texture id yet (first update) or the
617                    // id isn't registered, create it now and write back the TexID,
618                    // so this frame (or the next) can bind the correct texture.
619                    if internal_id == 0 || !self.contains_texture(internal_id) {
620                        match self.create_texture_from_data(device, queue, texture_data) {
621                            Ok(new_id) => {
622                                texture_data.set_tex_id(dear_imgui_rs::TextureId::from(new_id));
623                                texture_data.set_status(TextureStatus::OK);
624                            }
625                            Err(_e) => {
626                                // Leave it destroyed to avoid retry storm; user can request create again
627                                texture_data.set_status(TextureStatus::Destroyed);
628                            }
629                        }
630                    } else {
631                        // Try in-place sub-rect updates first
632                        if self
633                            .apply_subrect_updates(queue, texture_data, internal_id)
634                            .unwrap_or(false)
635                        {
636                            texture_data.set_status(TextureStatus::OK);
637                        } else if self
638                            .update_texture_from_data_with_id(
639                                device,
640                                queue,
641                                texture_data,
642                                internal_id,
643                            )
644                            .is_err()
645                        {
646                            // If update fails, mark as destroyed
647                            texture_data.set_status(TextureStatus::Destroyed);
648                        } else {
649                            texture_data.set_status(TextureStatus::OK);
650                        }
651                    }
652                }
653                TextureStatus::WantDestroy => {
654                    // Only destroy when unused frames > 0 (align with official backend behavior)
655                    let mut can_destroy = true;
656                    unsafe {
657                        let raw = texture_data.as_raw();
658                        if !raw.is_null() {
659                            // If field not present in bindings on some versions, default true
660                            #[allow(unused_unsafe)]
661                            {
662                                // Access UnusedFrames if available
663                                // SAFETY: reading a plain field from raw C struct
664                                can_destroy = (*raw).UnusedFrames > 0;
665                            }
666                        }
667                    }
668                    if can_destroy {
669                        let imgui_tex_id = texture_data.tex_id();
670                        let internal_id = imgui_tex_id.id();
671                        // Remove from cache
672                        self.remove_texture(internal_id);
673                        texture_data.set_status(TextureStatus::Destroyed);
674                    }
675                }
676                TextureStatus::OK | TextureStatus::Destroyed => {
677                    // No action needed
678                }
679            }
680        }
681    }
682
683    /// Update a single texture based on its status
684    ///
685    /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
686    ///
687    /// # Returns
688    ///
689    /// Returns a `TextureUpdateResult` that contains the operation result and
690    /// any status/ID updates that need to be applied to the texture data.
691    /// This follows Rust's principle of explicit state management.
692    ///
693    /// # Example
694    ///
695    /// ```rust,no_run
696    /// # use dear_imgui_wgpu::*;
697    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
698    /// # let mut texture_manager = WgpuTextureManager::new();
699    /// # let device = todo!();
700    /// # let queue = todo!();
701    /// # let mut texture_data = dear_imgui_rs::TextureData::new();
702    /// let result = texture_manager.update_single_texture(&texture_data, &device, &queue)?;
703    /// result.apply_to(&mut texture_data);
704    /// # Ok(())
705    /// # }
706    /// ```
707    pub fn update_single_texture(
708        &mut self,
709        texture_data: &dear_imgui_rs::TextureData,
710        device: &Device,
711        queue: &Queue,
712    ) -> Result<TextureUpdateResult, String> {
713        match texture_data.status() {
714            TextureStatus::WantCreate => {
715                match self.create_texture_from_data(device, queue, texture_data) {
716                    Ok(texture_id) => Ok(TextureUpdateResult::Created {
717                        texture_id: TextureId::from(texture_id),
718                    }),
719                    Err(e) => Err(format!("Failed to create texture: {}", e)),
720                }
721            }
722            TextureStatus::WantUpdates => {
723                let internal_id = texture_data.tex_id().id();
724                if internal_id == 0 || !self.contains_texture(internal_id) {
725                    // No valid ID yet: create now and return Created so caller can set TexID
726                    match self.create_texture_from_data(device, queue, texture_data) {
727                        Ok(texture_id) => Ok(TextureUpdateResult::Created {
728                            texture_id: TextureId::from(texture_id),
729                        }),
730                        Err(e) => Err(format!("Failed to create texture: {}", e)),
731                    }
732                } else {
733                    match self.update_texture_from_data_with_id(
734                        device,
735                        queue,
736                        texture_data,
737                        internal_id,
738                    ) {
739                        Ok(_) => Ok(TextureUpdateResult::Updated),
740                        Err(_e) => Ok(TextureUpdateResult::Failed),
741                    }
742                }
743            }
744            TextureStatus::WantDestroy => {
745                self.destroy_texture(texture_data.tex_id());
746                Ok(TextureUpdateResult::Destroyed)
747            }
748            TextureStatus::OK | TextureStatus::Destroyed => Ok(TextureUpdateResult::NoAction),
749        }
750    }
751}