Skip to main content

dear_imgui_wgpu/texture/
upload.rs

1use super::*;
2
3impl WgpuTextureManager {
4    /// Convert a sub-rectangle of ImGui texture pixels into a tightly packed RGBA8 buffer
5    pub(super) fn convert_subrect_to_rgba(
6        texture_data: &TextureData,
7        rect: dear_imgui_rs::texture::TextureRect,
8    ) -> Option<Vec<u8>> {
9        let pixels = texture_data.pixels()?;
10        let tex_w = usize::try_from(texture_data.width()).ok()?;
11        let tex_h = usize::try_from(texture_data.height()).ok()?;
12        if tex_w == 0 || tex_h == 0 {
13            return None;
14        }
15
16        let bpp = texture_data.bytes_per_pixel();
17        let (rx, ry, rw, rh) = (
18            rect.x as usize,
19            rect.y as usize,
20            rect.w as usize,
21            rect.h as usize,
22        );
23        if rw == 0 || rh == 0 || rx >= tex_w || ry >= tex_h {
24            return None;
25        }
26
27        // Clamp to texture bounds defensively
28        let rw = rw.min(tex_w.saturating_sub(rx));
29        let rh = rh.min(tex_h.saturating_sub(ry));
30
31        let mut out = vec![0u8; rw.checked_mul(rh)?.checked_mul(4)?];
32        match texture_data.format() {
33            ImGuiTextureFormat::RGBA32 => {
34                for row in 0..rh {
35                    let src_off = ((ry + row) * tex_w + rx) * bpp;
36                    let dst_off = row * rw * 4;
37                    // Copy only the row slice and convert layout if needed (it is already RGBA)
38                    out[dst_off..dst_off + rw * 4]
39                        .copy_from_slice(&pixels[src_off..src_off + rw * 4]);
40                }
41            }
42            ImGuiTextureFormat::Alpha8 => {
43                for row in 0..rh {
44                    let src_off = ((ry + row) * tex_w + rx) * bpp; // bpp = 1
45                    let dst_off = row * rw * 4;
46                    for i in 0..rw {
47                        let a = pixels[src_off + i];
48                        let dst = &mut out[dst_off + i * 4..dst_off + i * 4 + 4];
49                        dst.copy_from_slice(&[255, 255, 255, a]);
50                    }
51                }
52            }
53        }
54        Some(out)
55    }
56
57    /// Apply queued sub-rectangle updates to an existing WGPU texture.
58    /// Returns true if any update was applied.
59    pub(super) fn apply_subrect_updates(
60        &mut self,
61        queue: &Queue,
62        texture_data: &TextureData,
63        texture_id: TextureId,
64    ) -> RendererResult<bool> {
65        let wgpu_tex = match self.textures.get(&texture_id) {
66            Some(t) => t,
67            None => return Ok(false),
68        };
69
70        // Collect update rectangles; prefer explicit Updates[] if present,
71        // otherwise fallback to single UpdateRect.
72        let mut rects: Vec<dear_imgui_rs::texture::TextureRect> = texture_data.updates().collect();
73        if rects.is_empty() {
74            let r = texture_data.update_rect();
75            if r.w > 0 && r.h > 0 {
76                rects.push(r);
77            }
78        }
79        if rects.is_empty() {
80            return Ok(false);
81        }
82
83        // Upload each rect
84        for rect in rects {
85            if let Some(tight_rgba) = Self::convert_subrect_to_rgba(texture_data, rect) {
86                let width = rect.w as u32;
87                let height = rect.h as u32;
88                let bpp = 4u32;
89                let unpadded_bytes_per_row = width * bpp;
90                let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
91                let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
92
93                if padded_bytes_per_row == unpadded_bytes_per_row {
94                    // Aligned: direct upload
95                    queue.write_texture(
96                        wgpu::TexelCopyTextureInfo {
97                            texture: wgpu_tex.texture(),
98                            mip_level: 0,
99                            origin: wgpu::Origin3d {
100                                x: rect.x as u32,
101                                y: rect.y as u32,
102                                z: 0,
103                            },
104                            aspect: wgpu::TextureAspect::All,
105                        },
106                        &tight_rgba,
107                        wgpu::TexelCopyBufferLayout {
108                            offset: 0,
109                            bytes_per_row: Some(unpadded_bytes_per_row),
110                            rows_per_image: Some(height),
111                        },
112                        wgpu::Extent3d {
113                            width,
114                            height,
115                            depth_or_array_layers: 1,
116                        },
117                    );
118                } else {
119                    // Pad each row to the required alignment
120                    let mut padded = vec![0u8; (padded_bytes_per_row * height) as usize];
121                    for row in 0..height as usize {
122                        let src_off = row * (unpadded_bytes_per_row as usize);
123                        let dst_off = row * (padded_bytes_per_row as usize);
124                        padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)]
125                            .copy_from_slice(
126                                &tight_rgba[src_off..src_off + (unpadded_bytes_per_row as usize)],
127                            );
128                    }
129                    queue.write_texture(
130                        wgpu::TexelCopyTextureInfo {
131                            texture: wgpu_tex.texture(),
132                            mip_level: 0,
133                            origin: wgpu::Origin3d {
134                                x: rect.x as u32,
135                                y: rect.y as u32,
136                                z: 0,
137                            },
138                            aspect: wgpu::TextureAspect::All,
139                        },
140                        &padded,
141                        wgpu::TexelCopyBufferLayout {
142                            offset: 0,
143                            bytes_per_row: Some(padded_bytes_per_row),
144                            rows_per_image: Some(height),
145                        },
146                        wgpu::Extent3d {
147                            width,
148                            height,
149                            depth_or_array_layers: 1,
150                        },
151                    );
152                }
153                if cfg!(debug_assertions) {
154                    tracing::debug!(
155                        target: "dear-imgui-wgpu",
156                        "[dear-imgui-wgpu][debug] Updated texture id={} subrect x={} y={} w={} h={}",
157                        texture_id.id(), rect.x, rect.y, rect.w, rect.h
158                    );
159                }
160            } else {
161                // No pixels available, cannot update this rect
162                if cfg!(debug_assertions) {
163                    tracing::debug!(
164                        target: "dear-imgui-wgpu",
165                        "[dear-imgui-wgpu][debug] Skipped subrect update: no pixels available"
166                    );
167                }
168                return Ok(false);
169            }
170        }
171
172        Ok(true)
173    }
174
175    /// Update an existing texture from Dear ImGui texture data with specific ID
176    pub fn update_texture_from_data_with_id(
177        &mut self,
178        device: &Device,
179        queue: &Queue,
180        texture_data: &TextureData,
181        texture_id: TextureId,
182    ) -> RendererResult<()> {
183        // For WGPU, we recreate the texture instead of updating in place.
184        // Create the replacement first so a failure does not destroy the existing texture.
185        if self.contains_texture(texture_id) {
186            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
187
188            // Move the texture to the correct ID slot if needed.
189            if new_texture_id != texture_id
190                && let Some(texture) = self.remove_texture(new_texture_id)
191            {
192                self.remove_texture(texture_id);
193                self.insert_texture_with_id(texture_id, texture);
194            }
195
196            Ok(())
197        } else {
198            Err(RendererError::InvalidTextureId(texture_id))
199        }
200    }
201
202    /// Texture creation and management functions
203    /// Create a texture from Dear ImGui texture data
204    pub fn create_texture_from_data(
205        &mut self,
206        device: &Device,
207        queue: &Queue,
208        texture_data: &TextureData,
209    ) -> RendererResult<TextureId> {
210        let width = texture_data.width();
211        let height = texture_data.height();
212        let format = texture_data.format();
213
214        let pixels = texture_data
215            .pixels()
216            .ok_or_else(|| RendererError::BadTexture("No pixel data available".to_string()))?;
217
218        // Convert ImGui texture format to WGPU format and handle data conversion
219        // This matches the texture format handling in imgui_impl_wgpu.cpp
220        let (wgpu_format, converted_data, _bytes_per_pixel) = match format {
221            ImGuiTextureFormat::RGBA32 => {
222                // RGBA32 maps directly to RGBA8Unorm (matches C++ implementation)
223                let expected_len = usize::try_from(width)
224                    .ok()
225                    .and_then(|w| usize::try_from(height).ok().and_then(|h| w.checked_mul(h)))
226                    .and_then(|px| px.checked_mul(4))
227                    .ok_or_else(|| {
228                        RendererError::BadTexture("RGBA32 texture size overflow".to_string())
229                    })?;
230                if pixels.len() != expected_len {
231                    return Err(RendererError::BadTexture(format!(
232                        "RGBA32 texture data size mismatch: expected {} bytes, got {}",
233                        expected_len,
234                        pixels.len()
235                    )));
236                }
237                (TextureFormat::Rgba8Unorm, pixels.to_vec(), 4u32)
238            }
239            ImGuiTextureFormat::Alpha8 => {
240                // Convert Alpha8 to RGBA32 for WGPU (white RGB + original alpha)
241                // This ensures compatibility with the standard RGBA8Unorm format
242                let expected_len = usize::try_from(width)
243                    .ok()
244                    .and_then(|w| usize::try_from(height).ok().and_then(|h| w.checked_mul(h)))
245                    .ok_or_else(|| {
246                        RendererError::BadTexture("Alpha8 texture size overflow".to_string())
247                    })?;
248                if pixels.len() != expected_len {
249                    return Err(RendererError::BadTexture(format!(
250                        "Alpha8 texture data size mismatch: expected {} bytes, got {}",
251                        expected_len,
252                        pixels.len()
253                    )));
254                }
255                let mut rgba_data = Vec::with_capacity(pixels.len() * 4);
256                for &alpha in pixels {
257                    rgba_data.extend_from_slice(&[255, 255, 255, alpha]); // White RGB + alpha
258                }
259                (TextureFormat::Rgba8Unorm, rgba_data, 4u32)
260            }
261        };
262
263        // Create WGPU texture (matches the descriptor setup in imgui_impl_wgpu.cpp)
264        if cfg!(debug_assertions) {
265            tracing::debug!(
266                target: "dear-imgui-wgpu",
267                "[dear-imgui-wgpu][debug] Create texture: {}x{} format={:?}",
268                width, height, format
269            );
270        }
271        let texture = device.create_texture(&TextureDescriptor {
272            label: Some("Dear ImGui Texture"),
273            size: Extent3d {
274                width,
275                height,
276                depth_or_array_layers: 1,
277            },
278            mip_level_count: 1,
279            sample_count: 1,
280            dimension: TextureDimension::D2,
281            format: wgpu_format,
282            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
283            view_formats: &[],
284        });
285
286        // Validate texture data size before upload
287        let expected_size = usize::try_from(width)
288            .ok()
289            .and_then(|w| usize::try_from(height).ok().and_then(|h| w.checked_mul(h)))
290            .and_then(|px| px.checked_mul(4))
291            .ok_or_else(|| {
292                RendererError::BadTexture("Converted texture size overflow".to_string())
293            })?; // Always RGBA after conversion
294        if converted_data.len() != expected_size {
295            return Err(RendererError::BadTexture(format!(
296                "Converted texture data size mismatch: expected {} bytes, got {}",
297                expected_size,
298                converted_data.len()
299            )));
300        }
301
302        // Upload texture data (matches the upload logic in imgui_impl_wgpu.cpp)
303        // WebGPU requires bytes_per_row to be 256-byte aligned. Pad rows if needed.
304        let bpp = 4u32;
305        let unpadded_bytes_per_row = width * bpp;
306        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; // 256
307        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
308        if padded_bytes_per_row == unpadded_bytes_per_row {
309            // Aligned: direct upload
310            queue.write_texture(
311                wgpu::TexelCopyTextureInfo {
312                    texture: &texture,
313                    mip_level: 0,
314                    origin: wgpu::Origin3d::ZERO,
315                    aspect: wgpu::TextureAspect::All,
316                },
317                &converted_data,
318                wgpu::TexelCopyBufferLayout {
319                    offset: 0,
320                    bytes_per_row: Some(unpadded_bytes_per_row),
321                    rows_per_image: Some(height),
322                },
323                Extent3d {
324                    width,
325                    height,
326                    depth_or_array_layers: 1,
327                },
328            );
329        } else {
330            // Pad each row to the required alignment
331            let mut padded: Vec<u8> = vec![0; (padded_bytes_per_row * height) as usize];
332            for row in 0..height as usize {
333                let src_off = row * (unpadded_bytes_per_row as usize);
334                let dst_off = row * (padded_bytes_per_row as usize);
335                padded[dst_off..dst_off + (unpadded_bytes_per_row as usize)].copy_from_slice(
336                    &converted_data[src_off..src_off + (unpadded_bytes_per_row as usize)],
337                );
338            }
339            queue.write_texture(
340                wgpu::TexelCopyTextureInfo {
341                    texture: &texture,
342                    mip_level: 0,
343                    origin: wgpu::Origin3d::ZERO,
344                    aspect: wgpu::TextureAspect::All,
345                },
346                &padded,
347                wgpu::TexelCopyBufferLayout {
348                    offset: 0,
349                    bytes_per_row: Some(padded_bytes_per_row),
350                    rows_per_image: Some(height),
351                },
352                Extent3d {
353                    width,
354                    height,
355                    depth_or_array_layers: 1,
356                },
357            );
358            if cfg!(debug_assertions) {
359                tracing::debug!(
360                    target: "dear-imgui-wgpu",
361                    "[dear-imgui-wgpu][debug] Upload texture with padded row pitch: unpadded={} padded={}",
362                    unpadded_bytes_per_row, padded_bytes_per_row
363                );
364            }
365        }
366
367        // Create texture view
368        let texture_view = texture.create_view(&TextureViewDescriptor::default());
369
370        // Create WGPU texture wrapper
371        let wgpu_texture = WgpuTexture::new(texture, texture_view);
372
373        // Register and return ID
374        let texture_id = self.register_texture(wgpu_texture);
375        if cfg!(debug_assertions) {
376            tracing::debug!(
377                target: "dear-imgui-wgpu",
378                "[dear-imgui-wgpu][debug] Texture registered: id={}",
379                texture_id.id()
380            );
381        }
382        Ok(texture_id)
383    }
384
385    /// Update an existing texture from Dear ImGui texture data
386    pub fn update_texture_from_data(
387        &mut self,
388        device: &Device,
389        queue: &Queue,
390        texture_data: &TextureData,
391    ) -> RendererResult<()> {
392        let texture_id = texture_data.tex_id();
393
394        // If the texture already exists and the TextureData only requests sub-rectangle
395        // updates, honor them in-place to match Dear ImGui 1.92 semantics.
396        // Fallback to full re-create when there is no pixel data available.
397        if self.contains_texture(texture_id) {
398            // Attempt sub-rect updates first (preferred path)
399            if self.apply_subrect_updates(queue, texture_data, texture_id)? {
400                return Ok(());
401            }
402
403            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
404            if new_texture_id != texture_id
405                && let Some(texture) = self.remove_texture(new_texture_id)
406            {
407                self.remove_texture(texture_id);
408                self.insert_texture_with_id(texture_id, texture);
409            }
410        } else {
411            // Create new texture if it doesn't exist
412            let new_texture_id = self.create_texture_from_data(device, queue, texture_data)?;
413            if new_texture_id != texture_id
414                && let Some(texture) = self.remove_texture(new_texture_id)
415            {
416                self.insert_texture_with_id(texture_id, texture);
417            }
418        }
419
420        Ok(())
421    }
422
423    /// Update a single texture based on its status
424    ///
425    /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
426    ///
427    /// # Returns
428    ///
429    /// Returns a `TextureUpdateResult` that contains the operation result and
430    /// any status/ID updates that need to be applied to the texture data.
431    /// This follows Rust's principle of explicit state management.
432    ///
433    /// # Example
434    ///
435    /// ```rust,no_run
436    /// # use dear_imgui_wgpu::*;
437    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
438    /// # let mut texture_manager = WgpuTextureManager::new();
439    /// # let device = todo!();
440    /// # let queue = todo!();
441    /// # let mut texture_data = dear_imgui_rs::TextureData::new();
442    /// let result = texture_manager.update_single_texture(&texture_data, &device, &queue)?;
443    /// result.apply_to(&mut texture_data);
444    /// # Ok(())
445    /// # }
446    /// ```
447    pub fn update_single_texture(
448        &mut self,
449        texture_data: &dear_imgui_rs::TextureData,
450        device: &Device,
451        queue: &Queue,
452    ) -> RendererResult<TextureUpdateResult> {
453        match texture_data.status() {
454            TextureStatus::WantCreate => {
455                let texture_id = self.create_texture_from_data(device, queue, texture_data)?;
456                Ok(TextureUpdateResult::Created { texture_id })
457            }
458            TextureStatus::WantUpdates => {
459                let internal_id = texture_data.tex_id();
460                if internal_id.is_null() || !self.contains_texture(internal_id) {
461                    // No valid ID yet: create now and return Created so caller can set TexID
462                    let texture_id = self.create_texture_from_data(device, queue, texture_data)?;
463                    Ok(TextureUpdateResult::Created { texture_id })
464                } else {
465                    match self.update_texture_from_data_with_id(
466                        device,
467                        queue,
468                        texture_data,
469                        internal_id,
470                    ) {
471                        Ok(_) => Ok(TextureUpdateResult::Updated),
472                        Err(e) => Err(e),
473                    }
474                }
475            }
476            TextureStatus::WantDestroy => {
477                self.destroy_texture(texture_data.tex_id());
478                Ok(TextureUpdateResult::Destroyed)
479            }
480            TextureStatus::OK | TextureStatus::Destroyed => Ok(TextureUpdateResult::NoAction),
481        }
482    }
483}