blinc_gpu 0.5.0

Blinc GPU renderer - SDF-based rendering via wgpu
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Gradient texture cache for multi-stop gradient support
//!
//! This module provides a 1D texture-based gradient rasterization system
//! that enables paths to use gradients with more than 2 color stops.
//!
//! The approach follows standard techniques used in Skia and Cairo:
//! - Gradients are rasterized to a 256-wide 1D RGBA texture
//! - The shader samples from this texture using the gradient parameter t
//! - A placeholder texture is used for 2-stop gradients (fast path)

use blinc_core::{Color, GradientStop};
use lru::LruCache;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;

/// Maximum number of cached rasterized gradients
const GRADIENT_CACHE_CAPACITY: usize = 32;

/// How gradient colors are spread outside the gradient's defined range
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpreadMode {
    /// Clamp to edge colors (default)
    #[default]
    Pad,
    /// Repeat the gradient pattern
    Repeat,
    /// Mirror/reflect the gradient pattern
    Reflect,
}

/// Width of the gradient lookup texture
pub const GRADIENT_TEXTURE_WIDTH: u32 = 256;

/// Rasterized gradient data ready for GPU upload
pub struct RasterizedGradient {
    /// RGBA pixel data (256 * 4 bytes)
    pub pixels: [u8; GRADIENT_TEXTURE_WIDTH as usize * 4],
    /// Number of color stops in the original gradient
    pub stop_count: usize,
}

impl RasterizedGradient {
    /// Rasterize a gradient with multiple stops into a 256-wide texture
    pub fn from_stops(stops: &[GradientStop], spread: SpreadMode) -> Self {
        let mut pixels = [0u8; GRADIENT_TEXTURE_WIDTH as usize * 4];

        if stops.is_empty() {
            // All transparent
            return Self {
                pixels,
                stop_count: 0,
            };
        }

        if stops.len() == 1 {
            // Single color fills entire texture
            let c = &stops[0].color;
            let r = (c.r * 255.0).clamp(0.0, 255.0) as u8;
            let g = (c.g * 255.0).clamp(0.0, 255.0) as u8;
            let b = (c.b * 255.0).clamp(0.0, 255.0) as u8;
            let a = (c.a * 255.0).clamp(0.0, 255.0) as u8;

            for i in 0..GRADIENT_TEXTURE_WIDTH as usize {
                pixels[i * 4] = r;
                pixels[i * 4 + 1] = g;
                pixels[i * 4 + 2] = b;
                pixels[i * 4 + 3] = a;
            }

            return Self {
                pixels,
                stop_count: 1,
            };
        }

        // Rasterize multi-stop gradient
        for i in 0..GRADIENT_TEXTURE_WIDTH as usize {
            let t = i as f32 / (GRADIENT_TEXTURE_WIDTH - 1) as f32;

            // Apply spread mode
            let t = apply_spread_mode(t, spread);

            // Find the two stops that bracket t
            let color = sample_gradient(stops, t);

            pixels[i * 4] = (color.r * 255.0).clamp(0.0, 255.0) as u8;
            pixels[i * 4 + 1] = (color.g * 255.0).clamp(0.0, 255.0) as u8;
            pixels[i * 4 + 2] = (color.b * 255.0).clamp(0.0, 255.0) as u8;
            pixels[i * 4 + 3] = (color.a * 255.0).clamp(0.0, 255.0) as u8;
        }

        Self {
            pixels,
            stop_count: stops.len(),
        }
    }

    /// Create a simple 2-stop gradient
    pub fn two_stop(start: Color, end: Color) -> Self {
        let stops = [
            GradientStop {
                offset: 0.0,
                color: start,
            },
            GradientStop {
                offset: 1.0,
                color: end,
            },
        ];
        Self::from_stops(&stops, SpreadMode::Pad)
    }
}

/// Apply spread mode to a gradient parameter
fn apply_spread_mode(t: f32, spread: SpreadMode) -> f32 {
    match spread {
        SpreadMode::Pad => t.clamp(0.0, 1.0),
        SpreadMode::Repeat => t.fract().abs(),
        SpreadMode::Reflect => {
            let t_mod = t.abs() % 2.0;
            if t_mod > 1.0 {
                2.0 - t_mod
            } else {
                t_mod
            }
        }
    }
}

/// Sample a gradient at parameter t
fn sample_gradient(stops: &[GradientStop], t: f32) -> Color {
    if stops.is_empty() {
        return Color::TRANSPARENT;
    }

    if t <= stops[0].offset {
        return stops[0].color;
    }

    if t >= stops[stops.len() - 1].offset {
        return stops[stops.len() - 1].color;
    }

    // Find bracketing stops
    for i in 0..stops.len() - 1 {
        let s0 = &stops[i];
        let s1 = &stops[i + 1];

        if t >= s0.offset && t <= s1.offset {
            // Interpolate between these stops
            let range = s1.offset - s0.offset;
            if range < 0.0001 {
                return s0.color;
            }

            let local_t = (t - s0.offset) / range;
            return lerp_color(&s0.color, &s1.color, local_t);
        }
    }

    // Fallback
    stops[stops.len() - 1].color
}

/// Linear interpolation between two colors
fn lerp_color(a: &Color, b: &Color, t: f32) -> Color {
    Color {
        r: a.r + (b.r - a.r) * t,
        g: a.g + (b.g - a.g) * t,
        b: a.b + (b.b - a.b) * t,
        a: a.a + (b.a - a.a) * t,
    }
}

/// Compute a hash for gradient stops (offset + RGBA values)
fn hash_gradient_stops(stops: &[GradientStop], spread: SpreadMode) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    let mut hasher = DefaultHasher::new();

    // Hash spread mode
    (spread as u8).hash(&mut hasher);

    // Hash each stop's offset and color components
    for stop in stops {
        // Convert f32 to bits for deterministic hashing
        stop.offset.to_bits().hash(&mut hasher);
        stop.color.r.to_bits().hash(&mut hasher);
        stop.color.g.to_bits().hash(&mut hasher);
        stop.color.b.to_bits().hash(&mut hasher);
        stop.color.a.to_bits().hash(&mut hasher);
    }

    hasher.finish()
}

/// GPU gradient texture cache with LRU caching for rasterized gradients
pub struct GradientTextureCache {
    /// The gradient texture (256x1 RGBA)
    pub texture: wgpu::Texture,
    /// Texture view for binding
    pub view: wgpu::TextureView,
    /// Sampler for gradient lookups
    pub sampler: wgpu::Sampler,
    /// Whether the texture contains valid gradient data
    pub has_gradient: bool,
    /// LRU cache for rasterized gradient pixel data (avoids re-rasterizing)
    rasterized_cache: LruCache<u64, Box<[u8; GRADIENT_TEXTURE_WIDTH as usize * 4]>>,
    /// Hash of the currently uploaded gradient (to avoid redundant uploads)
    current_hash: Option<u64>,
}

impl GradientTextureCache {
    /// Create a new gradient texture cache with a placeholder texture
    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Gradient Texture"),
            size: wgpu::Extent3d {
                width: GRADIENT_TEXTURE_WIDTH,
                height: 1,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D1,
            format: wgpu::TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });

        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("Gradient Sampler"),
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });

        // Initialize with a white-to-white gradient (placeholder)
        let placeholder = RasterizedGradient::two_stop(Color::WHITE, Color::WHITE);
        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &placeholder.pixels,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
                rows_per_image: Some(1),
            },
            wgpu::Extent3d {
                width: GRADIENT_TEXTURE_WIDTH,
                height: 1,
                depth_or_array_layers: 1,
            },
        );

        Self {
            texture,
            view,
            sampler,
            has_gradient: false,
            rasterized_cache: LruCache::new(NonZeroUsize::new(GRADIENT_CACHE_CAPACITY).unwrap()),
            current_hash: None,
        }
    }

    /// Upload gradient stops with caching (avoids re-rasterizing identical gradients)
    ///
    /// Returns true if the gradient was uploaded, false if it was already current
    pub fn upload_stops(
        &mut self,
        queue: &wgpu::Queue,
        stops: &[GradientStop],
        spread: SpreadMode,
    ) -> bool {
        let hash = hash_gradient_stops(stops, spread);

        // Skip upload if this gradient is already on the GPU
        if self.current_hash == Some(hash) {
            return false;
        }

        // Look up or rasterize the gradient
        let pixels: &[u8; GRADIENT_TEXTURE_WIDTH as usize * 4] =
            if let Some(cached) = self.rasterized_cache.get(&hash) {
                cached.as_ref()
            } else {
                // Rasterize and cache
                let rasterized = RasterizedGradient::from_stops(stops, spread);
                self.rasterized_cache.put(hash, Box::new(rasterized.pixels));
                self.rasterized_cache.get(&hash).unwrap().as_ref()
            };

        // Upload to GPU
        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &self.texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            pixels,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
                rows_per_image: Some(1),
            },
            wgpu::Extent3d {
                width: GRADIENT_TEXTURE_WIDTH,
                height: 1,
                depth_or_array_layers: 1,
            },
        );

        self.has_gradient = stops.len() > 2;
        self.current_hash = Some(hash);
        true
    }

    /// Upload a rasterized gradient to the GPU texture (legacy method)
    pub fn upload(&mut self, queue: &wgpu::Queue, gradient: &RasterizedGradient) {
        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &self.texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &gradient.pixels,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(GRADIENT_TEXTURE_WIDTH * 4),
                rows_per_image: Some(1),
            },
            wgpu::Extent3d {
                width: GRADIENT_TEXTURE_WIDTH,
                height: 1,
                depth_or_array_layers: 1,
            },
        );
        self.has_gradient = gradient.stop_count > 2;
    }

    /// Clear the gradient texture (sets to placeholder)
    pub fn clear(&mut self, queue: &wgpu::Queue) {
        let placeholder = RasterizedGradient::two_stop(Color::WHITE, Color::WHITE);
        self.upload(queue, &placeholder);
        self.has_gradient = false;
        self.current_hash = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_two_stop_gradient() {
        let gradient = RasterizedGradient::two_stop(Color::BLACK, Color::WHITE);
        assert_eq!(gradient.stop_count, 2);

        // First pixel should be black
        assert_eq!(gradient.pixels[0], 0); // R
        assert_eq!(gradient.pixels[1], 0); // G
        assert_eq!(gradient.pixels[2], 0); // B
        assert_eq!(gradient.pixels[3], 255); // A

        // Last pixel should be white
        let last_idx = (GRADIENT_TEXTURE_WIDTH as usize - 1) * 4;
        assert_eq!(gradient.pixels[last_idx], 255); // R
        assert_eq!(gradient.pixels[last_idx + 1], 255); // G
        assert_eq!(gradient.pixels[last_idx + 2], 255); // B
        assert_eq!(gradient.pixels[last_idx + 3], 255); // A
    }

    #[test]
    fn test_multi_stop_gradient() {
        let stops = vec![
            GradientStop {
                offset: 0.0,
                color: Color::RED,
            },
            GradientStop {
                offset: 0.5,
                color: Color::GREEN,
            },
            GradientStop {
                offset: 1.0,
                color: Color::BLUE,
            },
        ];

        let gradient = RasterizedGradient::from_stops(&stops, SpreadMode::Pad);
        assert_eq!(gradient.stop_count, 3);

        // First pixel should be red
        assert!(gradient.pixels[0] > 200); // R
        assert!(gradient.pixels[1] < 50); // G
        assert!(gradient.pixels[2] < 50); // B

        // Middle pixel should be greenish
        let mid_idx = 128 * 4;
        assert!(gradient.pixels[mid_idx + 1] > gradient.pixels[mid_idx]); // G > R

        // Last pixel should be blue
        let last_idx = 255 * 4;
        assert!(gradient.pixels[last_idx] < 50); // R
        assert!(gradient.pixels[last_idx + 1] < 50); // G
        assert!(gradient.pixels[last_idx + 2] > 200); // B
    }
}