Skip to main content

cuda_rust_wasm/memory/
texture_memory.rs

1//! Texture memory for GPU-style 2D/3D data access with interpolation
2//!
3//! Provides a software emulation of CUDA texture memory, supporting
4//! nearest-neighbor and bilinear filtering, clamping and wrapping address
5//! modes, and normalized coordinate access.
6
7use crate::{Result, memory_error};
8use std::sync::Arc;
9
10/// Texture addressing mode
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum AddressMode {
13    /// Clamp to edge (repeat edge texels)
14    Clamp,
15    /// Wrap around (modulo)
16    Wrap,
17    /// Mirror at boundaries
18    Mirror,
19    /// Return zero outside [0, dim)
20    Border,
21}
22
23/// Texture filter mode
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FilterMode {
26    /// Nearest-neighbor sampling (point)
27    Point,
28    /// Bilinear interpolation
29    Linear,
30}
31
32/// Texture descriptor
33#[derive(Debug, Clone)]
34pub struct TextureDescriptor {
35    pub width: usize,
36    pub height: usize,
37    pub depth: usize,
38    pub address_mode: AddressMode,
39    pub filter_mode: FilterMode,
40    pub normalized_coords: bool,
41}
42
43impl TextureDescriptor {
44    /// Create a 1D texture descriptor
45    pub fn new_1d(width: usize) -> Self {
46        Self {
47            width,
48            height: 1,
49            depth: 1,
50            address_mode: AddressMode::Clamp,
51            filter_mode: FilterMode::Point,
52            normalized_coords: false,
53        }
54    }
55
56    /// Create a 2D texture descriptor
57    pub fn new_2d(width: usize, height: usize) -> Self {
58        Self {
59            width,
60            height,
61            depth: 1,
62            address_mode: AddressMode::Clamp,
63            filter_mode: FilterMode::Point,
64            normalized_coords: false,
65        }
66    }
67
68    /// Create a 3D texture descriptor
69    pub fn new_3d(width: usize, height: usize, depth: usize) -> Self {
70        Self {
71            width,
72            height,
73            depth,
74            address_mode: AddressMode::Clamp,
75            filter_mode: FilterMode::Point,
76            normalized_coords: false,
77        }
78    }
79
80    /// Set address mode
81    pub fn with_address_mode(mut self, mode: AddressMode) -> Self {
82        self.address_mode = mode;
83        self
84    }
85
86    /// Set filter mode
87    pub fn with_filter_mode(mut self, mode: FilterMode) -> Self {
88        self.filter_mode = mode;
89        self
90    }
91
92    /// Enable normalized coordinates
93    pub fn with_normalized_coords(mut self, normalized: bool) -> Self {
94        self.normalized_coords = normalized;
95        self
96    }
97}
98
99/// Texture memory object providing GPU-style texture sampling
100///
101/// Supports 1D, 2D, and 3D textures with configurable addressing
102/// and filtering modes. Data is stored as `f32` values internally.
103pub struct TextureMemory {
104    data: Vec<f32>,
105    descriptor: TextureDescriptor,
106}
107
108impl TextureMemory {
109    /// Create a new texture from data and descriptor
110    pub fn new(data: Vec<f32>, descriptor: TextureDescriptor) -> Result<Self> {
111        let expected = descriptor.width * descriptor.height * descriptor.depth;
112        if data.len() != expected {
113            return Err(memory_error!(
114                "Texture data length {} doesn't match dimensions {}x{}x{} = {}",
115                data.len(), descriptor.width, descriptor.height, descriptor.depth, expected
116            ));
117        }
118        Ok(Self { data, descriptor })
119    }
120
121    /// Create a zeroed texture
122    pub fn zeroed(descriptor: TextureDescriptor) -> Self {
123        let size = descriptor.width * descriptor.height * descriptor.depth;
124        Self {
125            data: vec![0.0; size],
126            descriptor,
127        }
128    }
129
130    /// Get texture descriptor
131    pub fn descriptor(&self) -> &TextureDescriptor {
132        &self.descriptor
133    }
134
135    /// Get width
136    pub fn width(&self) -> usize {
137        self.descriptor.width
138    }
139
140    /// Get height
141    pub fn height(&self) -> usize {
142        self.descriptor.height
143    }
144
145    /// Get depth
146    pub fn depth(&self) -> usize {
147        self.descriptor.depth
148    }
149
150    /// Bind data to the texture (copy from slice)
151    pub fn bind(&mut self, data: &[f32]) -> Result<()> {
152        let expected = self.descriptor.width * self.descriptor.height * self.descriptor.depth;
153        if data.len() != expected {
154            return Err(memory_error!(
155                "Data length {} doesn't match texture size {}",
156                data.len(), expected
157            ));
158        }
159        self.data.copy_from_slice(data);
160        Ok(())
161    }
162
163    /// Sample the texture at 1D coordinate
164    pub fn sample_1d(&self, x: f32) -> f32 {
165        let fx = if self.descriptor.normalized_coords {
166            x * self.descriptor.width as f32
167        } else {
168            x
169        };
170
171        match self.descriptor.filter_mode {
172            FilterMode::Point => {
173                let ix = self.address_coord(fx.round() as isize, self.descriptor.width);
174                self.data[ix]
175            }
176            FilterMode::Linear => {
177                let x0 = fx.floor();
178                let frac = fx - x0;
179                let i0 = self.address_coord(x0 as isize, self.descriptor.width);
180                let i1 = self.address_coord(x0 as isize + 1, self.descriptor.width);
181                self.data[i0] * (1.0 - frac) + self.data[i1] * frac
182            }
183        }
184    }
185
186    /// Sample the texture at 2D coordinates
187    pub fn sample_2d(&self, x: f32, y: f32) -> f32 {
188        let fx = if self.descriptor.normalized_coords {
189            x * self.descriptor.width as f32
190        } else {
191            x
192        };
193        let fy = if self.descriptor.normalized_coords {
194            y * self.descriptor.height as f32
195        } else {
196            y
197        };
198
199        match self.descriptor.filter_mode {
200            FilterMode::Point => {
201                let ix = self.address_coord(fx.round() as isize, self.descriptor.width);
202                let iy = self.address_coord(fy.round() as isize, self.descriptor.height);
203                self.data[iy * self.descriptor.width + ix]
204            }
205            FilterMode::Linear => {
206                let x0 = fx.floor();
207                let y0 = fy.floor();
208                let fx_frac = fx - x0;
209                let fy_frac = fy - y0;
210
211                let ix0 = self.address_coord(x0 as isize, self.descriptor.width);
212                let ix1 = self.address_coord(x0 as isize + 1, self.descriptor.width);
213                let iy0 = self.address_coord(y0 as isize, self.descriptor.height);
214                let iy1 = self.address_coord(y0 as isize + 1, self.descriptor.height);
215
216                let w = self.descriptor.width;
217                let v00 = self.data[iy0 * w + ix0];
218                let v10 = self.data[iy0 * w + ix1];
219                let v01 = self.data[iy1 * w + ix0];
220                let v11 = self.data[iy1 * w + ix1];
221
222                let top = v00 * (1.0 - fx_frac) + v10 * fx_frac;
223                let bot = v01 * (1.0 - fx_frac) + v11 * fx_frac;
224                top * (1.0 - fy_frac) + bot * fy_frac
225            }
226        }
227    }
228
229    /// Sample the texture at 3D coordinates
230    pub fn sample_3d(&self, x: f32, y: f32, z: f32) -> f32 {
231        let fx = if self.descriptor.normalized_coords {
232            x * self.descriptor.width as f32
233        } else {
234            x
235        };
236        let fy = if self.descriptor.normalized_coords {
237            y * self.descriptor.height as f32
238        } else {
239            y
240        };
241        let fz = if self.descriptor.normalized_coords {
242            z * self.descriptor.depth as f32
243        } else {
244            z
245        };
246
247        let ix = self.address_coord(fx.round() as isize, self.descriptor.width);
248        let iy = self.address_coord(fy.round() as isize, self.descriptor.height);
249        let iz = self.address_coord(fz.round() as isize, self.descriptor.depth);
250
251        let w = self.descriptor.width;
252        let h = self.descriptor.height;
253        self.data[iz * w * h + iy * w + ix]
254    }
255
256    /// Read raw data back
257    pub fn read_data(&self) -> &[f32] {
258        &self.data
259    }
260
261    /// Write to a specific texel
262    pub fn write_texel(&mut self, x: usize, y: usize, value: f32) -> Result<()> {
263        if x >= self.descriptor.width || y >= self.descriptor.height {
264            return Err(memory_error!(
265                "Texel ({}, {}) out of bounds ({}x{})",
266                x, y, self.descriptor.width, self.descriptor.height
267            ));
268        }
269        self.data[y * self.descriptor.width + x] = value;
270        Ok(())
271    }
272
273    /// Apply address mode to a coordinate
274    fn address_coord(&self, coord: isize, dim: usize) -> usize {
275        let d = dim as isize;
276        match self.descriptor.address_mode {
277            AddressMode::Clamp => coord.clamp(0, d - 1) as usize,
278            AddressMode::Wrap => ((coord % d + d) % d) as usize,
279            AddressMode::Mirror => {
280                let c = ((coord % (2 * d) + 2 * d) % (2 * d)) as usize;
281                if c < dim { c } else { 2 * dim - c - 1 }
282            }
283            AddressMode::Border => {
284                if coord < 0 || coord >= d { 0 } else { coord as usize }
285            }
286        }
287    }
288}
289
290/// Shared texture handle
291pub type SharedTexture = Arc<TextureMemory>;
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_texture_1d_point_sampling() {
299        let data = vec![1.0, 2.0, 3.0, 4.0];
300        let desc = TextureDescriptor::new_1d(4);
301        let tex = TextureMemory::new(data, desc).unwrap();
302
303        assert_eq!(tex.sample_1d(0.0), 1.0);
304        assert_eq!(tex.sample_1d(1.0), 2.0);
305        assert_eq!(tex.sample_1d(3.0), 4.0);
306    }
307
308    #[test]
309    fn test_texture_1d_linear_sampling() {
310        let data = vec![0.0, 10.0, 20.0, 30.0];
311        let desc = TextureDescriptor::new_1d(4).with_filter_mode(FilterMode::Linear);
312        let tex = TextureMemory::new(data, desc).unwrap();
313
314        assert!((tex.sample_1d(0.5) - 5.0).abs() < 1e-5);
315        assert!((tex.sample_1d(1.5) - 15.0).abs() < 1e-5);
316    }
317
318    #[test]
319    fn test_texture_2d_point_sampling() {
320        let data = vec![
321            1.0, 2.0, 3.0,
322            4.0, 5.0, 6.0,
323        ];
324        let desc = TextureDescriptor::new_2d(3, 2);
325        let tex = TextureMemory::new(data, desc).unwrap();
326
327        assert_eq!(tex.sample_2d(0.0, 0.0), 1.0);
328        assert_eq!(tex.sample_2d(2.0, 0.0), 3.0);
329        assert_eq!(tex.sample_2d(0.0, 1.0), 4.0);
330        assert_eq!(tex.sample_2d(2.0, 1.0), 6.0);
331    }
332
333    #[test]
334    fn test_texture_2d_bilinear_sampling() {
335        let data = vec![
336            0.0, 10.0,
337            10.0, 20.0,
338        ];
339        let desc = TextureDescriptor::new_2d(2, 2).with_filter_mode(FilterMode::Linear);
340        let tex = TextureMemory::new(data, desc).unwrap();
341
342        // Center should be average of all four
343        let center = tex.sample_2d(0.5, 0.5);
344        assert!((center - 10.0).abs() < 1e-5);
345    }
346
347    #[test]
348    fn test_texture_address_clamp() {
349        let data = vec![1.0, 2.0, 3.0, 4.0];
350        let desc = TextureDescriptor::new_1d(4).with_address_mode(AddressMode::Clamp);
351        let tex = TextureMemory::new(data, desc).unwrap();
352
353        // Out of bounds should clamp to edge
354        assert_eq!(tex.sample_1d(-1.0), 1.0);
355        assert_eq!(tex.sample_1d(10.0), 4.0);
356    }
357
358    #[test]
359    fn test_texture_address_wrap() {
360        let data = vec![10.0, 20.0, 30.0, 40.0];
361        let desc = TextureDescriptor::new_1d(4).with_address_mode(AddressMode::Wrap);
362        let tex = TextureMemory::new(data, desc).unwrap();
363
364        assert_eq!(tex.sample_1d(4.0), 10.0); // wraps to 0
365        assert_eq!(tex.sample_1d(5.0), 20.0); // wraps to 1
366    }
367
368    #[test]
369    fn test_texture_normalized_coords() {
370        let data = vec![1.0, 2.0, 3.0, 4.0];
371        let desc = TextureDescriptor::new_1d(4).with_normalized_coords(true);
372        let tex = TextureMemory::new(data, desc).unwrap();
373
374        // 0.0 maps to index 0, 0.5 maps to index 2
375        assert_eq!(tex.sample_1d(0.0), 1.0);
376        assert_eq!(tex.sample_1d(0.5), 3.0);
377    }
378
379    #[test]
380    fn test_texture_bind_data() {
381        let desc = TextureDescriptor::new_1d(4);
382        let mut tex = TextureMemory::zeroed(desc);
383
384        assert_eq!(tex.sample_1d(0.0), 0.0);
385        tex.bind(&[5.0, 6.0, 7.0, 8.0]).unwrap();
386        assert_eq!(tex.sample_1d(0.0), 5.0);
387        assert_eq!(tex.sample_1d(3.0), 8.0);
388    }
389
390    #[test]
391    fn test_texture_write_texel() {
392        let desc = TextureDescriptor::new_2d(4, 4);
393        let mut tex = TextureMemory::zeroed(desc);
394
395        tex.write_texel(2, 1, 42.0).unwrap();
396        assert_eq!(tex.sample_2d(2.0, 1.0), 42.0);
397    }
398
399    #[test]
400    fn test_texture_write_texel_out_of_bounds() {
401        let desc = TextureDescriptor::new_2d(4, 4);
402        let mut tex = TextureMemory::zeroed(desc);
403
404        assert!(tex.write_texel(10, 0, 1.0).is_err());
405    }
406
407    #[test]
408    fn test_texture_data_size_mismatch() {
409        let desc = TextureDescriptor::new_2d(3, 3);
410        let result = TextureMemory::new(vec![0.0; 5], desc);
411        assert!(result.is_err());
412    }
413
414    #[test]
415    fn test_texture_3d_sampling() {
416        let data = vec![0.0; 2 * 2 * 2];
417        let desc = TextureDescriptor::new_3d(2, 2, 2);
418        let mut tex = TextureMemory::new(data, desc).unwrap();
419
420        // Write to (1, 1, 1)
421        tex.data[1 * 2 * 2 + 1 * 2 + 1] = 99.0;
422        assert_eq!(tex.sample_3d(1.0, 1.0, 1.0), 99.0);
423    }
424}