1use spatialrust_core::{SpatialError, SpatialResult};
4use spatialrust_image::{Image, ImageMetadata, ImageView};
5
6use crate::WgpuRuntime;
7
8const BYTES_PER_TEXEL: u64 = 4;
9
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
12pub struct GpuImageReceipt {
13 host_to_device_bytes: u64,
14 gpu_to_gpu_bytes: u64,
15 device_to_host_bytes: u64,
16 stages: Vec<&'static str>,
17}
18
19impl GpuImageReceipt {
20 #[must_use]
22 pub const fn host_to_device_bytes(&self) -> u64 {
23 self.host_to_device_bytes
24 }
25
26 #[must_use]
28 pub const fn gpu_to_gpu_bytes(&self) -> u64 {
29 self.gpu_to_gpu_bytes
30 }
31
32 #[must_use]
34 pub const fn device_to_host_bytes(&self) -> u64 {
35 self.device_to_host_bytes
36 }
37
38 #[must_use]
40 pub fn stages(&self) -> &[&'static str] {
41 &self.stages
42 }
43
44 pub fn validate_resident_chain(&self, expected_upload_bytes: u64) -> SpatialResult<()> {
48 if self.host_to_device_bytes != expected_upload_bytes {
49 return Err(SpatialError::InvalidArgument(format!(
50 "resident chain expected {expected_upload_bytes} host-to-device bytes, recorded {}",
51 self.host_to_device_bytes
52 )));
53 }
54 if self.device_to_host_bytes != 0 {
55 return Err(SpatialError::InvalidArgument(format!(
56 "resident chain forbids device-to-host transfers, recorded {} bytes",
57 self.device_to_host_bytes
58 )));
59 }
60 Ok(())
61 }
62
63 pub(crate) fn record_host_to_device(&mut self, bytes: u64, stage: &'static str) {
64 self.host_to_device_bytes = self.host_to_device_bytes.saturating_add(bytes);
65 self.stages.push(stage);
66 }
67
68 pub(crate) fn record_gpu_to_gpu(&mut self, bytes: u64, stage: &'static str) {
69 self.gpu_to_gpu_bytes = self.gpu_to_gpu_bytes.saturating_add(bytes);
70 self.stages.push(stage);
71 }
72
73 pub(crate) fn record_device_to_host(&mut self, bytes: u64, stage: &'static str) {
74 self.device_to_host_bytes = self.device_to_host_bytes.saturating_add(bytes);
75 self.stages.push(stage);
76 }
77
78 pub(crate) fn merge_from(&mut self, other: &Self) {
79 self.host_to_device_bytes =
80 self.host_to_device_bytes.saturating_add(other.host_to_device_bytes);
81 self.gpu_to_gpu_bytes = self.gpu_to_gpu_bytes.saturating_add(other.gpu_to_gpu_bytes);
82 self.device_to_host_bytes =
83 self.device_to_host_bytes.saturating_add(other.device_to_host_bytes);
84 self.stages.extend_from_slice(&other.stages);
85 }
86}
87
88pub struct GpuImage {
93 width: u32,
94 height: u32,
95 channels: u32,
96 device_key: usize,
97 texture: wgpu::Texture,
98 storage_bytes: u64,
99 metadata: ImageMetadata,
100 receipt: GpuImageReceipt,
101}
102
103impl GpuImage {
104 pub fn upload_u8<const CHANNELS: usize>(
106 runtime: &WgpuRuntime,
107 view: ImageView<'_, u8, CHANNELS>,
108 ) -> SpatialResult<Self> {
109 if CHANNELS == 0 || CHANNELS > 4 {
110 return Err(SpatialError::InvalidArgument(
111 "GpuImage upload supports 1..=4 channels".to_owned(),
112 ));
113 }
114 let width = u32::try_from(view.width())
115 .map_err(|_| SpatialError::InvalidArgument("GpuImage width exceeds u32".to_owned()))?;
116 let height = u32::try_from(view.height())
117 .map_err(|_| SpatialError::InvalidArgument("GpuImage height exceeds u32".to_owned()))?;
118 if width == 0 || height == 0 {
119 return Err(SpatialError::InvalidArgument(
120 "GpuImage upload requires positive width and height".to_owned(),
121 ));
122 }
123 let texture = create_texture(runtime, width, height, "gpu-image-upload");
124 let texels = pack_rgba8(view);
125 runtime.queue().write_texture(
126 wgpu::TexelCopyTextureInfo {
127 texture: &texture,
128 mip_level: 0,
129 origin: wgpu::Origin3d::ZERO,
130 aspect: wgpu::TextureAspect::All,
131 },
132 &texels,
133 wgpu::TexelCopyBufferLayout {
134 offset: 0,
135 bytes_per_row: Some(width * BYTES_PER_TEXEL as u32),
136 rows_per_image: Some(height),
137 },
138 extent(width, height),
139 );
140 let storage_bytes = texture_bytes(width, height);
141 let mut receipt = GpuImageReceipt::default();
142 receipt.record_host_to_device(storage_bytes, "upload_u8_texture");
143 Ok(Self {
144 width,
145 height,
146 channels: CHANNELS as u32,
147 device_key: runtime_device_key(runtime),
148 texture,
149 storage_bytes,
150 metadata: view.metadata(),
151 receipt,
152 })
153 }
154
155 pub fn readback_u8<const CHANNELS: usize>(
157 &mut self,
158 runtime: &WgpuRuntime,
159 ) -> SpatialResult<Image<u8, CHANNELS>> {
160 self.validate_runtime(runtime)?;
161 if self.channels as usize != CHANNELS {
162 return Err(SpatialError::InvalidArgument(format!(
163 "GpuImage has {} channels but readback requested {CHANNELS}",
164 self.channels
165 )));
166 }
167 let unpadded_row = self.width * BYTES_PER_TEXEL as u32;
168 let padded_row = unpadded_row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
169 * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
170 let staging_size = u64::from(padded_row) * u64::from(self.height);
171 let staging = runtime.device().create_buffer(&wgpu::BufferDescriptor {
172 label: Some("gpu-image-texture-readback"),
173 size: staging_size,
174 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
175 mapped_at_creation: false,
176 });
177 let mut encoder =
178 runtime.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
179 label: Some("gpu-image-texture-readback-encoder"),
180 });
181 encoder.copy_texture_to_buffer(
182 wgpu::TexelCopyTextureInfo {
183 texture: &self.texture,
184 mip_level: 0,
185 origin: wgpu::Origin3d::ZERO,
186 aspect: wgpu::TextureAspect::All,
187 },
188 wgpu::TexelCopyBufferInfo {
189 buffer: &staging,
190 layout: wgpu::TexelCopyBufferLayout {
191 offset: 0,
192 bytes_per_row: Some(padded_row),
193 rows_per_image: Some(self.height),
194 },
195 },
196 extent(self.width, self.height),
197 );
198 runtime.queue().submit(Some(encoder.finish()));
199 let rgba = read_staging_bytes(runtime.device(), &staging, staging_size as usize)?;
200 let mut data = Vec::with_capacity(self.width as usize * self.height as usize * CHANNELS);
201 for row in rgba.chunks_exact(padded_row as usize).take(self.height as usize) {
202 for texel in row[..unpadded_row as usize].chunks_exact(BYTES_PER_TEXEL as usize) {
203 data.extend_from_slice(&texel[..CHANNELS]);
204 }
205 }
206 self.receipt.record_device_to_host(self.storage_bytes, "readback_u8_texture");
207 Image::try_new_with_metadata(self.width as usize, self.height as usize, data, self.metadata)
208 .map_err(|error| SpatialError::InvalidArgument(error.to_string()))
209 }
210
211 #[must_use]
213 pub const fn width(&self) -> u32 {
214 self.width
215 }
216
217 #[must_use]
219 pub const fn height(&self) -> u32 {
220 self.height
221 }
222
223 #[must_use]
225 pub const fn channels(&self) -> u32 {
226 self.channels
227 }
228
229 #[must_use]
231 pub const fn metadata(&self) -> ImageMetadata {
232 self.metadata
233 }
234
235 #[must_use]
237 pub const fn receipt(&self) -> &GpuImageReceipt {
238 &self.receipt
239 }
240
241 pub fn recycle(self, runtime: &WgpuRuntime) {
243 if self.validate_runtime(runtime).is_ok() {
244 runtime.recycle_image_texture(self.width, self.height, self.texture);
245 }
246 }
247
248 pub(crate) fn validate_runtime(&self, runtime: &WgpuRuntime) -> SpatialResult<()> {
249 if self.device_key != runtime_device_key(runtime) {
250 return Err(SpatialError::InvalidArgument(
251 "GpuImage belongs to a different runtime device".to_owned(),
252 ));
253 }
254 Ok(())
255 }
256
257 pub(crate) const fn storage_bytes(&self) -> u64 {
258 self.storage_bytes
259 }
260
261 pub(crate) const fn texture(&self) -> &wgpu::Texture {
262 &self.texture
263 }
264
265 pub(crate) fn view(&self) -> wgpu::TextureView {
266 self.texture.create_view(&wgpu::TextureViewDescriptor::default())
267 }
268
269 pub(crate) fn from_parts(
270 runtime: &WgpuRuntime,
271 width: u32,
272 height: u32,
273 channels: u32,
274 texture: wgpu::Texture,
275 metadata: ImageMetadata,
276 receipt: GpuImageReceipt,
277 ) -> SpatialResult<Self> {
278 if width == 0 || height == 0 || !(1..=4).contains(&channels) {
279 return Err(SpatialError::InvalidArgument(
280 "GpuImage dimensions/channels must be positive with 1..=4 channels".to_owned(),
281 ));
282 }
283 Ok(Self {
284 width,
285 height,
286 channels,
287 device_key: runtime_device_key(runtime),
288 texture,
289 storage_bytes: texture_bytes(width, height),
290 metadata,
291 receipt,
292 })
293 }
294}
295
296pub(crate) fn create_texture(
297 runtime: &WgpuRuntime,
298 width: u32,
299 height: u32,
300 label: &'static str,
301) -> wgpu::Texture {
302 runtime.acquire_image_texture(width, height, label)
303}
304
305fn pack_rgba8<const CHANNELS: usize>(view: ImageView<'_, u8, CHANNELS>) -> Vec<u8> {
306 let mut packed = vec![0_u8; view.width() * view.height() * BYTES_PER_TEXEL as usize];
307 for y in 0..view.height() {
308 let source = view.row(y).expect("input row in bounds");
309 let target = &mut packed[y * view.width() * 4..(y + 1) * view.width() * 4];
310 for (pixel, texel) in source.chunks_exact(CHANNELS).zip(target.chunks_exact_mut(4)) {
311 texel[..CHANNELS].copy_from_slice(pixel);
312 }
313 }
314 packed
315}
316
317const fn extent(width: u32, height: u32) -> wgpu::Extent3d {
318 wgpu::Extent3d { width, height, depth_or_array_layers: 1 }
319}
320
321const fn texture_bytes(width: u32, height: u32) -> u64 {
322 width as u64 * height as u64 * BYTES_PER_TEXEL
323}
324
325pub(crate) fn runtime_device_key(runtime: &WgpuRuntime) -> usize {
326 runtime.device() as *const wgpu::Device as usize
327}
328
329pub(crate) fn read_staging_bytes(
330 device: &wgpu::Device,
331 staging_buffer: &wgpu::Buffer,
332 len: usize,
333) -> SpatialResult<Vec<u8>> {
334 let slice = staging_buffer.slice(..);
335 let (sender, receiver) = std::sync::mpsc::channel();
336 slice.map_async(wgpu::MapMode::Read, move |result| {
337 let _ = sender.send(result);
338 });
339 device.poll(wgpu::Maintain::Wait);
340 receiver
341 .recv()
342 .map_err(|_| SpatialError::InvalidArgument("failed to receive wgpu map result".to_owned()))?
343 .map_err(|error| {
344 SpatialError::InvalidArgument(format!("failed to map wgpu texture: {error}"))
345 })?;
346 let data = slice.get_mapped_range();
347 let values = data[..len].to_vec();
348 drop(data);
349 staging_buffer.unmap();
350 Ok(values)
351}