Skip to main content

oxiui_render_wgpu/gpu/
texture.rs

1//! Texture upload utilities for the textured pipeline.
2//!
3//! [`upload_image`] converts an [`ImageData`] into a GPU texture + sampler pair
4//! ready to bind in a render pass.  `queue.write_texture` is used directly — it
5//! does **not** require 256-byte row alignment (unlike `copy_buffer_to_texture`).
6//!
7//! [`TexturedDraw`] carries the pre-built [`TexVertex`] list alongside the
8//! image data and per-draw state needed by the render loop in `renderer.rs`.
9
10use crate::gpu::buffer::TexVertex;
11use crate::gpu::device::TARGET_FORMAT;
12use oxiui_core::paint::{ImageData, ImageFilter};
13use oxiui_core::UiError;
14
15// ── TexturedDraw ──────────────────────────────────────────────────────────────
16
17/// One textured draw call: a vertex list plus the source image and draw state.
18///
19/// Built during `build_geometry` and consumed in the textured pass of
20/// `execute()`.
21pub struct TexturedDraw {
22    /// Pre-built vertices (2 triangles per quad, 9 × 6 for nine-slice).
23    pub verts: Vec<TexVertex>,
24    /// Owned copy of the source image.
25    pub image: ImageData,
26    /// Texture filter to apply when uploading and sampling.
27    pub filter: ImageFilter,
28    /// Optional hardware scissor rect `[x, y, w, h]` in physical pixels.
29    pub scissor: Option<[u32; 4]>,
30}
31
32// ── upload_image ──────────────────────────────────────────────────────────────
33
34/// Upload `image` to a new `Rgba8Unorm` GPU texture and create a matching
35/// sampler.
36///
37/// Uses `queue.write_texture` which does NOT require row-alignment padding —
38/// `bytes_per_row = image.width * 4` is correct here.
39///
40/// # Errors
41///
42/// Returns [`UiError::Render`] when the image dimensions are zero.
43pub fn upload_image(
44    device: &wgpu::Device,
45    queue: &wgpu::Queue,
46    image: &ImageData,
47    filter: ImageFilter,
48) -> Result<(wgpu::TextureView, wgpu::Sampler), UiError> {
49    if image.width == 0 || image.height == 0 {
50        return Err(UiError::Render("zero-dimension image".to_string()));
51    }
52
53    let texture = device.create_texture(&wgpu::TextureDescriptor {
54        label: Some("oxiui-render-wgpu uploaded texture"),
55        size: wgpu::Extent3d {
56            width: image.width,
57            height: image.height,
58            depth_or_array_layers: 1,
59        },
60        mip_level_count: 1,
61        sample_count: 1,
62        dimension: wgpu::TextureDimension::D2,
63        format: TARGET_FORMAT,
64        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
65        view_formats: &[],
66    });
67
68    queue.write_texture(
69        wgpu::TexelCopyTextureInfo {
70            texture: &texture,
71            mip_level: 0,
72            origin: wgpu::Origin3d::ZERO,
73            aspect: wgpu::TextureAspect::All,
74        },
75        &image.rgba,
76        wgpu::TexelCopyBufferLayout {
77            offset: 0,
78            bytes_per_row: Some(image.width * 4),
79            rows_per_image: Some(image.height),
80        },
81        wgpu::Extent3d {
82            width: image.width,
83            height: image.height,
84            depth_or_array_layers: 1,
85        },
86    );
87
88    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
89
90    let filter_mode = match filter {
91        ImageFilter::Nearest => wgpu::FilterMode::Nearest,
92        ImageFilter::Bilinear => wgpu::FilterMode::Linear,
93    };
94
95    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
96        label: Some("oxiui-render-wgpu texture sampler"),
97        address_mode_u: wgpu::AddressMode::ClampToEdge,
98        address_mode_v: wgpu::AddressMode::ClampToEdge,
99        address_mode_w: wgpu::AddressMode::ClampToEdge,
100        mag_filter: filter_mode,
101        min_filter: filter_mode,
102        mipmap_filter: wgpu::MipmapFilterMode::Nearest,
103        lod_min_clamp: 0.0,
104        lod_max_clamp: 32.0,
105        compare: None,
106        anisotropy_clamp: 1,
107        border_color: None,
108    });
109
110    Ok((view, sampler))
111}