Skip to main content

blinc_gpu/
image.rs

1//! Image texture management for GPU rendering
2//!
3//! Manages GPU textures for images and provides rendering support.
4
5use std::sync::Arc;
6use wgpu::util::DeviceExt;
7
8/// A GPU image texture ready for rendering
9pub struct GpuImage {
10    /// The GPU texture
11    texture: wgpu::Texture,
12    /// Texture view for sampling
13    view: wgpu::TextureView,
14    /// Image width
15    width: u32,
16    /// Image height
17    height: u32,
18}
19
20impl GpuImage {
21    /// Create a GPU image from RGBA pixel data
22    pub fn from_rgba(
23        device: &wgpu::Device,
24        queue: &wgpu::Queue,
25        pixels: &[u8],
26        width: u32,
27        height: u32,
28        label: Option<&str>,
29    ) -> Self {
30        let texture = device.create_texture_with_data(
31            queue,
32            &wgpu::TextureDescriptor {
33                label,
34                size: wgpu::Extent3d {
35                    width,
36                    height,
37                    depth_or_array_layers: 1,
38                },
39                mip_level_count: 1,
40                sample_count: 1,
41                dimension: wgpu::TextureDimension::D2,
42                format: wgpu::TextureFormat::Rgba8Unorm,
43                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
44                view_formats: &[],
45            },
46            wgpu::util::TextureDataOrder::LayerMajor,
47            pixels,
48        );
49
50        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
51
52        Self {
53            texture,
54            view,
55            width,
56            height,
57        }
58    }
59
60    /// Get the texture view for binding
61    pub fn view(&self) -> &wgpu::TextureView {
62        &self.view
63    }
64
65    /// Get image dimensions
66    pub fn dimensions(&self) -> (u32, u32) {
67        (self.width, self.height)
68    }
69
70    /// Get image width
71    pub fn width(&self) -> u32 {
72        self.width
73    }
74
75    /// Get image height
76    pub fn height(&self) -> u32 {
77        self.height
78    }
79
80    /// Get the underlying texture
81    pub fn texture(&self) -> &wgpu::Texture {
82        &self.texture
83    }
84}
85
86/// GPU image instance data for batched rendering
87///
88/// Memory layout (matches shader ImageInstance):
89/// - `dst_rect`: `vec4<f32>` (16 bytes) - destination rectangle
90/// - `src_uv`: `vec4<f32>` (16 bytes) - source UV coordinates
91/// - `tint`: `vec4<f32>` (16 bytes) - tint color
92/// - `params`: `vec4<f32>` (16 bytes) - border_radius, opacity, border_width, packed_border_color
93/// - `clip_bounds`: `vec4<f32>` (16 bytes) - clip region
94/// - `clip_radius`: `vec4<f32>` (16 bytes) - clip corner radii
95/// - `filter_a`: `vec4<f32>` (16 bytes) - grayscale, invert, sepia, hue_rotate_rad
96/// - `filter_b`: `vec4<f32>` (16 bytes) - brightness, contrast, saturate, unused
97/// - `transform`: `vec4<f32>` (16 bytes) - 2x2 affine matrix [a, b, c, d]
98/// - `clip2_bounds`: `vec4<f32>` (16 bytes) - secondary sharp clip (scroll boundary)
99/// - `mask_params`: `vec4<f32>` (16 bytes) - mask gradient geometry
100/// - `mask_info`: `vec4<f32>` (16 bytes) - mask type and alpha values
101///   Total: 192 bytes
102#[repr(C)]
103#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
104pub struct GpuImageInstance {
105    /// Destination rectangle (x, y, width, height) in screen pixels
106    pub dst_rect: [f32; 4],
107    /// Source UV rectangle (u_min, v_min, u_max, v_max) normalized 0-1
108    pub src_uv: [f32; 4],
109    /// Tint color (RGBA)
110    pub tint: [f32; 4],
111    /// Parameters: (border_radius, opacity, border_width, packed_border_color)
112    pub params: [f32; 4],
113    /// Clip bounds (x, y, width, height) - set to large negative x for no clip
114    pub clip_bounds: [f32; 4],
115    /// Clip corner radii (top-left, top-right, bottom-right, bottom-left)
116    pub clip_radius: [f32; 4],
117    /// CSS filter A (grayscale, invert, sepia, hue_rotate_rad)
118    pub filter_a: [f32; 4],
119    /// CSS filter B (brightness, contrast, saturate, unused)
120    pub filter_b: [f32; 4],
121    /// 2x2 CSS affine transform [a, b, c, d] applied around quad center.
122    /// Identity = [1, 0, 0, 1]. Supports rotation, scale, and skew.
123    pub transform: [f32; 4],
124    /// Secondary clip bounds (x, y, width, height) — sharp rect, no radius.
125    /// Used for scroll container boundaries separate from the primary rounded clip.
126    /// Set to large negative x for no clip.
127    pub clip2_bounds: [f32; 4],
128    /// Mask gradient params: linear=(x1,y1,x2,y2), radial=(cx,cy,r,0) in OBB space
129    pub mask_params: [f32; 4],
130    /// Mask info: [mask_type, start_alpha, end_alpha, 0] (0=none, 1=linear, 2=radial)
131    pub mask_info: [f32; 4],
132}
133
134impl Default for GpuImageInstance {
135    fn default() -> Self {
136        Self {
137            dst_rect: [0.0, 0.0, 100.0, 100.0],
138            src_uv: [0.0, 0.0, 1.0, 1.0],
139            tint: [1.0, 1.0, 1.0, 1.0],
140            params: [0.0, 1.0, 0.0, 0.0], // border_radius=0, opacity=1, border_width=0, border_color=0
141            // Default: no clip (large negative value disables clipping)
142            clip_bounds: [-10000.0, -10000.0, 100000.0, 100000.0],
143            clip_radius: [0.0; 4],
144            // Default filter: identity (no effect)
145            filter_a: [0.0, 0.0, 0.0, 0.0], // grayscale=0, invert=0, sepia=0, hue_rotate=0
146            filter_b: [1.0, 1.0, 1.0, 0.0], // brightness=1, contrast=1, saturate=1, unused=0
147            // Default transform: identity (no rotation, scale, or skew)
148            transform: [1.0, 0.0, 0.0, 1.0], // [a, b, c, d] = identity
149            // Default: no secondary clip
150            clip2_bounds: [-10000.0, -10000.0, 100000.0, 100000.0],
151            // Default: no mask gradient
152            mask_params: [0.0; 4],
153            mask_info: [0.0; 4],
154        }
155    }
156}
157
158impl GpuImageInstance {
159    /// Create a new image instance with no transformations
160    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
161        Self {
162            dst_rect: [x, y, width, height],
163            ..Default::default()
164        }
165    }
166
167    /// Set the source UV coordinates for cropping
168    pub fn with_src_uv(mut self, u_min: f32, v_min: f32, u_max: f32, v_max: f32) -> Self {
169        self.src_uv = [u_min, v_min, u_max, v_max];
170        self
171    }
172
173    /// Set a tint color
174    pub fn with_tint(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
175        self.tint = [r, g, b, a];
176        self
177    }
178
179    /// Set border radius for rounded corners
180    pub fn with_border_radius(mut self, radius: f32) -> Self {
181        self.params[0] = radius;
182        self
183    }
184
185    /// Set opacity
186    pub fn with_opacity(mut self, opacity: f32) -> Self {
187        self.params[1] = opacity;
188        self
189    }
190
191    /// Set border (rendered in the image shader for perfect transform alignment).
192    /// params\[2\] = border_width, params\[3\] = RGBA packed as u32 bitcast to f32.
193    pub fn with_image_border(mut self, width: f32, r: f32, g: f32, b: f32, a: f32) -> Self {
194        self.params[2] = width;
195        let ru = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
196        let gu = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
197        let bu = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
198        let au = (a.clamp(0.0, 1.0) * 255.0).round() as u32;
199        self.params[3] = f32::from_bits((ru << 24) | (gu << 16) | (bu << 8) | au);
200        self
201    }
202
203    /// Set full 2x2 affine transform [a, b, c, d] applied around quad center.
204    /// Supports rotation, scale, and skew. Identity = [1, 0, 0, 1].
205    pub fn with_transform(mut self, a: f32, b: f32, c: f32, d: f32) -> Self {
206        self.transform = [a, b, c, d];
207        self
208    }
209
210    /// Set rectangular clip region
211    pub fn with_clip_rect(mut self, x: f32, y: f32, width: f32, height: f32) -> Self {
212        self.clip_bounds = [x, y, width, height];
213        self.clip_radius = [0.0; 4];
214        self
215    }
216
217    /// Set rounded rectangular clip region with uniform radius
218    pub fn with_clip_rounded_rect(
219        mut self,
220        x: f32,
221        y: f32,
222        width: f32,
223        height: f32,
224        radius: f32,
225    ) -> Self {
226        self.clip_bounds = [x, y, width, height];
227        self.clip_radius = [radius; 4];
228        self
229    }
230
231    /// Set rounded rectangular clip region with per-corner radii
232    #[allow(clippy::too_many_arguments)]
233    pub fn with_clip_rounded_rect_corners(
234        mut self,
235        x: f32,
236        y: f32,
237        width: f32,
238        height: f32,
239        tl: f32,
240        tr: f32,
241        br: f32,
242        bl: f32,
243    ) -> Self {
244        self.clip_bounds = [x, y, width, height];
245        self.clip_radius = [tl, tr, br, bl];
246        self
247    }
248
249    /// Clear clip region (no clipping)
250    pub fn with_no_clip(mut self) -> Self {
251        self.clip_bounds = [-10000.0, -10000.0, 100000.0, 100000.0];
252        self.clip_radius = [0.0; 4];
253        self
254    }
255
256    /// Set secondary sharp clip (scroll container boundary, no radius)
257    pub fn with_clip2_rect(mut self, x: f32, y: f32, width: f32, height: f32) -> Self {
258        self.clip2_bounds = [x, y, width, height];
259        self
260    }
261
262    /// Set CSS filter parameters
263    /// filter_a = (grayscale, invert, sepia, hue_rotate_rad)
264    /// filter_b = (brightness, contrast, saturate, 0)
265    pub fn with_filter(mut self, filter_a: [f32; 4], filter_b: [f32; 4]) -> Self {
266        self.filter_a = filter_a;
267        self.filter_b = filter_b;
268        self
269    }
270
271    /// Get border radius
272    pub fn border_radius(&self) -> f32 {
273        self.params[0]
274    }
275
276    /// Get opacity
277    pub fn opacity(&self) -> f32 {
278        self.params[1]
279    }
280}
281
282/// Image rendering context
283pub struct ImageRenderingContext {
284    /// Device reference
285    device: Arc<wgpu::Device>,
286    /// Queue reference
287    queue: Arc<wgpu::Queue>,
288    /// Image sampler (linear filtering)
289    sampler_linear: wgpu::Sampler,
290    /// Image sampler (nearest filtering, for pixel art)
291    sampler_nearest: wgpu::Sampler,
292}
293
294impl ImageRenderingContext {
295    /// Create a new image rendering context
296    pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>) -> Self {
297        let sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
298            label: Some("Image Sampler (Linear)"),
299            address_mode_u: wgpu::AddressMode::ClampToEdge,
300            address_mode_v: wgpu::AddressMode::ClampToEdge,
301            address_mode_w: wgpu::AddressMode::ClampToEdge,
302            mag_filter: wgpu::FilterMode::Linear,
303            min_filter: wgpu::FilterMode::Linear,
304            mipmap_filter: wgpu::FilterMode::Linear,
305            ..Default::default()
306        });
307
308        let sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
309            label: Some("Image Sampler (Nearest)"),
310            address_mode_u: wgpu::AddressMode::ClampToEdge,
311            address_mode_v: wgpu::AddressMode::ClampToEdge,
312            address_mode_w: wgpu::AddressMode::ClampToEdge,
313            mag_filter: wgpu::FilterMode::Nearest,
314            min_filter: wgpu::FilterMode::Nearest,
315            mipmap_filter: wgpu::FilterMode::Nearest,
316            ..Default::default()
317        });
318
319        Self {
320            device,
321            queue,
322            sampler_linear,
323            sampler_nearest,
324        }
325    }
326
327    /// Create a GPU image from RGBA data
328    pub fn create_image(&self, pixels: &[u8], width: u32, height: u32) -> GpuImage {
329        GpuImage::from_rgba(&self.device, &self.queue, pixels, width, height, None)
330    }
331
332    /// Create a GPU image with a label
333    pub fn create_image_labeled(
334        &self,
335        pixels: &[u8],
336        width: u32,
337        height: u32,
338        label: &str,
339    ) -> GpuImage {
340        GpuImage::from_rgba(
341            &self.device,
342            &self.queue,
343            pixels,
344            width,
345            height,
346            Some(label),
347        )
348    }
349
350    /// Get the linear sampler
351    pub fn sampler_linear(&self) -> &wgpu::Sampler {
352        &self.sampler_linear
353    }
354
355    /// Get the nearest sampler
356    pub fn sampler_nearest(&self) -> &wgpu::Sampler {
357        &self.sampler_nearest
358    }
359
360    /// Get the device
361    pub fn device(&self) -> &wgpu::Device {
362        &self.device
363    }
364
365    /// Get the queue
366    pub fn queue(&self) -> &wgpu::Queue {
367        &self.queue
368    }
369}