rootvg_image/
texture.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use image::RgbaImage;
5use rootvg_core::math::PhysicalSizeU32;
6
7#[derive(Debug)]
8enum TextureSource {
9    Image {
10        data_to_upload: Option<RgbaImage>,
11        uploaded_texture: Option<wgpu::Texture>,
12    },
13    Prepass {
14        view: wgpu::TextureView,
15    },
16}
17
18#[derive(Debug)]
19pub(crate) struct TextureInner {
20    source: TextureSource,
21    pub(crate) bind_group: Option<wgpu::BindGroup>,
22}
23
24/// A source of raw image data.
25///
26/// Once this texture has been uploaded to the GPU, the image
27/// data will be automatically removed from RAM.
28#[derive(Debug)]
29pub struct RcTexture {
30    pub(crate) inner: Rc<RefCell<TextureInner>>,
31    size: PhysicalSizeU32,
32    generation: u64,
33}
34
35impl RcTexture {
36    pub fn new(image: impl Into<RgbaImage>) -> Self {
37        let image: RgbaImage = image.into();
38
39        let dimensions = image.dimensions();
40
41        Self {
42            inner: Rc::new(RefCell::new(TextureInner {
43                source: TextureSource::Image {
44                    data_to_upload: Some(image),
45                    uploaded_texture: None,
46                },
47                bind_group: None,
48            })),
49            size: PhysicalSizeU32::new(dimensions.0, dimensions.1),
50            generation: 0,
51        }
52    }
53
54    pub fn from_prepass_texture(texture_view: wgpu::TextureView, size: PhysicalSizeU32) -> Self {
55        Self {
56            inner: Rc::new(RefCell::new(TextureInner {
57                source: TextureSource::Prepass { view: texture_view },
58                bind_group: None,
59            })),
60            size,
61            generation: 0,
62        }
63    }
64
65    // TODO: Custom error
66    pub fn replace_with_image(&mut self, image: impl Into<RgbaImage>) -> Result<(), ()> {
67        let image: RgbaImage = image.into();
68        let dimensions = image.dimensions();
69        let size = PhysicalSizeU32::new(dimensions.0, dimensions.1);
70
71        if size != self.size {
72            return Err(());
73        }
74
75        let mut inner = RefCell::borrow_mut(&self.inner);
76
77        let TextureSource::Image { data_to_upload, .. } = &mut inner.source else {
78            return Err(());
79        };
80
81        *data_to_upload = Some(image);
82
83        self.generation += 1;
84
85        Ok(())
86    }
87
88    // TODO: Custom error
89    pub fn replace_prepass_texture(
90        &mut self,
91        texture_view: wgpu::TextureView,
92        size: PhysicalSizeU32,
93    ) -> Result<(), ()> {
94        if self.size != size {
95            return Err(());
96        }
97
98        let mut inner = RefCell::borrow_mut(&self.inner);
99
100        let TextureSource::Prepass { view } = &mut inner.source else {
101            return Err(());
102        };
103
104        *view = texture_view;
105
106        inner.bind_group = None;
107
108        self.generation += 1;
109
110        Ok(())
111    }
112
113    pub fn mark_prepass_texture_dirty(&mut self) {
114        self.generation += 1;
115    }
116
117    pub fn size(&self) -> PhysicalSizeU32 {
118        self.size
119    }
120
121    pub(crate) fn upload_if_needed(
122        &self,
123        device: &wgpu::Device,
124        queue: &wgpu::Queue,
125        texture_bind_group_layout: &wgpu::BindGroupLayout,
126    ) {
127        let mut inner = RefCell::borrow_mut(&self.inner);
128
129        let TextureInner { source, bind_group } = &mut *inner;
130
131        match source {
132            TextureSource::Image {
133                data_to_upload,
134                uploaded_texture,
135            } => {
136                let Some(data_to_upload) = data_to_upload.take() else {
137                    return;
138                };
139
140                if bind_group.is_none() {
141                    let dimensions = data_to_upload.dimensions();
142                    let texture_size = wgpu::Extent3d {
143                        width: dimensions.0,
144                        height: dimensions.1,
145                        depth_or_array_layers: 1,
146                    };
147
148                    let texture = device.create_texture(&wgpu::TextureDescriptor {
149                        // All textures are stored as 3D, we represent our 2D texture
150                        // by setting depth to 1.
151                        size: texture_size,
152                        mip_level_count: 1,
153                        sample_count: 1,
154                        dimension: wgpu::TextureDimension::D2,
155                        format: wgpu::TextureFormat::Rgba8UnormSrgb,
156                        // TEXTURE_BINDING tells wgpu that we want to use this texture in shaders
157                        // COPY_DST means that we want to copy data to this texture
158                        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
159                        label: None,
160                        view_formats: &[],
161                    });
162
163                    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
164
165                    let new_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
166                        layout: &texture_bind_group_layout,
167                        entries: &[wgpu::BindGroupEntry {
168                            binding: 0,
169                            resource: wgpu::BindingResource::TextureView(&view),
170                        }],
171                        label: None,
172                    });
173
174                    *bind_group = Some(new_bind_group);
175
176                    *uploaded_texture = Some(texture);
177                };
178
179                let uploaded_texture = uploaded_texture.as_ref().unwrap();
180
181                let texture_size = wgpu::Extent3d {
182                    width: self.size.width,
183                    height: self.size.height,
184                    depth_or_array_layers: 1,
185                };
186
187                queue.write_texture(
188                    wgpu::ImageCopyTexture {
189                        texture: uploaded_texture,
190                        mip_level: 0,
191                        origin: wgpu::Origin3d::ZERO,
192                        aspect: wgpu::TextureAspect::All,
193                    },
194                    &data_to_upload,
195                    // The layout of the texture
196                    wgpu::ImageDataLayout {
197                        offset: 0,
198                        bytes_per_row: Some(4 * self.size.width),
199                        rows_per_image: Some(self.size.height),
200                    },
201                    texture_size,
202                );
203            }
204            TextureSource::Prepass { view } => {
205                if bind_group.is_some() {
206                    return;
207                }
208
209                let new_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
210                    layout: &texture_bind_group_layout,
211                    entries: &[wgpu::BindGroupEntry {
212                        binding: 0,
213                        resource: wgpu::BindingResource::TextureView(view),
214                    }],
215                    label: None,
216                });
217
218                *bind_group = Some(new_bind_group);
219            }
220        }
221    }
222}
223
224impl Clone for RcTexture {
225    fn clone(&self) -> Self {
226        Self {
227            inner: Rc::clone(&self.inner),
228            size: self.size,
229            generation: self.generation,
230        }
231    }
232}
233
234impl PartialEq for RcTexture {
235    fn eq(&self, other: &Self) -> bool {
236        Rc::ptr_eq(&self.inner, &other.inner) && self.generation == other.generation
237    }
238}
239
240impl From<RgbaImage> for RcTexture {
241    fn from(image: RgbaImage) -> Self {
242        RcTexture::new(image)
243    }
244}