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
//! GPU image creation shared by rendering and decoding, independent of drawing caches.
use crate::images::{ImageDesc, IMAGE_FORMAT};
use crate::mips::{full_mip_count, MipGenerator};
use crate::pixels::{ImageError, PixelBuffer};
use std::sync::Arc;
use valo_dl::Image;
/// ImageContext turns pixels or textures into drawable images on the renderer's device.
///
/// It is the piece of the renderer an image decoder needs: the device, the queue and the mip
/// pipeline, and nothing that a frame in flight mutates. Obtain one from
/// `Context::image_context`; clones share the pipeline and may be used from any thread, so a
/// decode worker can upload its result and hand back a finished [`Image`].
#[derive(Clone)]
pub struct ImageContext {
device: wgpu::Device,
queue: wgpu::Queue,
mips: Arc<MipGenerator>,
}
impl ImageContext {
/// `new` creates image operations over a host-owned device and queue.
pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
let mips = Arc::new(MipGenerator::new(&device));
Self {
device,
queue,
mips,
}
}
/// `device` is the device every image made here belongs to.
pub fn device(&self) -> &wgpu::Device {
&self.device
}
/// `queue` is the queue image preparation commands are submitted on.
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
/// `check_size` rejects empty images and dimensions exceeding the device's limits.
pub fn check_size(&self, size: [u32; 2]) -> Result<(), ImageError> {
if size.contains(&0) {
return Err(ImageError::InvalidLayout);
}
let limit = self.device.limits().max_texture_dimension_2d;
if size.into_iter().any(|side| side > limit) {
return Err(ImageError::TooLarge);
}
Ok(())
}
/// `upload_pixels` consumes CPU samples and uploads a drawable image.
///
/// Channel order and alpha are normalized in the consumed allocation, so any layout a
/// decoder produced is accepted without a second buffer.
pub fn upload_pixels(&self, pixels: PixelBuffer, mipmaps: bool) -> Result<Image, ImageError> {
let layout = pixels.layout();
self.check_size(layout.size)?;
let rgba = pixels.into_premultiplied_rgba();
Ok(self.upload_rgba(
ImageDesc {
size: layout.size,
premultiplied: true,
mips: mipmaps,
},
&rgba,
))
}
/// `import_texture` retains a texture that already holds premultiplied sRGB samples.
///
/// The texture must be a single-sample 2D `Rgba8Unorm` or `Bgra8Unorm` texture with
/// `TEXTURE_BINDING` usage on this device. When `mipmaps` is set and the texture lacks a full
/// chain, level 0 is copied on the GPU into a new texture with the chain generated; the
/// original backing is never read back.
pub fn import_texture(
&self,
texture: wgpu::Texture,
mipmaps: bool,
) -> Result<Image, ImageError> {
check_sampleable(&texture)?;
let size = [texture.width(), texture.height()];
self.check_size(size)?;
#[cfg(feature = "trace")]
tracing::trace!(operation = "import", "image transfer");
let levels = texture.mip_level_count().max(1);
let image = Image::from_texture(texture, size, levels);
Ok(self.with_requested_mips(image, mipmaps))
}
/// `upload_rgba` creates a retained [`Image`] from RGBA8 pixels.
///
/// Premultiplies when `desc.premultiplied` is false, writes mip level 0,
/// and builds the mip chain when `desc.mips` is true. Panics if
/// `pixels.len()` is not `width * height * 4`.
pub(crate) fn upload_rgba(&self, desc: ImageDesc, pixels: &[u8]) -> Image {
let [w, h] = desc.size;
assert_eq!(pixels.len(), (w * h * 4) as usize, "RGBA8 pixel count");
let premultiplied = premultiplied_pixels(desc.premultiplied, pixels);
let mip_levels = if desc.mips {
full_mip_count(desc.size)
} else {
1
};
let texture = self.create_image_texture(desc.size, mip_levels);
self.write_level_zero(&texture, desc.size, &premultiplied);
self.finish_external(texture, desc.size, mip_levels)
}
/// `finish_external` wraps an already-populated texture as a retained [`Image`].
///
/// Use this when the host copied pixels itself (for example an
/// `ImageBitmap` upload). Builds the mip chain when `mip_levels` is
/// greater than 1.
pub(crate) fn finish_external(
&self,
texture: wgpu::Texture,
size: [u32; 2],
mip_levels: u32,
) -> Image {
if mip_levels > 1 {
self.mips
.generate(&self.device, &self.queue, &texture, mip_levels);
}
Image::from_texture(texture, size, mip_levels)
}
/// `regenerate_mips` rebuilds the mip chain after level 0 was rewritten in place.
///
/// Call this after each copy from a per-frame source such as a video frame.
pub(crate) fn regenerate_mips(&self, image: &Image) {
if image.mip_levels() > 1 {
self.mips.generate(
&self.device,
&self.queue,
image.texture(),
image.mip_levels(),
);
}
}
/// `create_image_texture` allocates an empty image texture of `size` and `mip_levels`.
///
/// The texture is bindable, copy-destination, and a render attachment so
/// mip levels can be generated by rendering into them.
pub(crate) fn create_image_texture(&self, size: [u32; 2], mip_levels: u32) -> wgpu::Texture {
self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("valo.image"),
size: extent(size),
mip_level_count: mip_levels,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: IMAGE_FORMAT,
// RENDER_ATTACHMENT: mip levels are generated by rendering into them.
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
/// Gives an imported image a full RGBA mip chain when asked for and not already present.
fn with_requested_mips(&self, image: Image, mipmaps: bool) -> Image {
let levels = if mipmaps {
full_mip_count(image.size())
} else {
1
};
if image.mip_levels() >= levels {
return image;
}
#[cfg(feature = "trace")]
tracing::trace!(operation = "copy_for_mipmaps", "image transfer");
let texture = self.create_image_texture(image.size(), levels);
self.mips
.copy_level_zero(&self.device, &self.queue, image.texture(), &texture);
self.finish_external(texture, image.size(), levels)
}
fn write_level_zero(&self, texture: &wgpu::Texture, size: [u32; 2], premultiplied: &[u8]) {
#[cfg(feature = "trace")]
tracing::trace!(operation = "upload", "image transfer");
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
premultiplied,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(size[0] * 4),
rows_per_image: None,
},
extent(size),
);
}
}
fn check_sampleable(texture: &wgpu::Texture) -> Result<(), ImageError> {
let shape_ok = texture.dimension() == wgpu::TextureDimension::D2
&& texture.depth_or_array_layers() == 1
&& texture.sample_count() == 1;
let usage_ok = texture
.usage()
.contains(wgpu::TextureUsages::TEXTURE_BINDING);
let format_ok = matches!(
texture.format(),
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
);
if shape_ok && usage_ok && format_ok {
Ok(())
} else {
Err(ImageError::IncompatibleTexture)
}
}
fn extent(size: [u32; 2]) -> wgpu::Extent3d {
wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
}
}
fn premultiplied_pixels(already: bool, pixels: &[u8]) -> std::borrow::Cow<'_, [u8]> {
if already {
return std::borrow::Cow::Borrowed(pixels);
}
let mut out = pixels.to_vec();
for px in out.chunks_exact_mut(4) {
let a = px[3] as u32;
px[0] = ((px[0] as u32 * a + 127) / 255) as u8;
px[1] = ((px[1] as u32 * a + 127) / 255) as u8;
px[2] = ((px[2] as u32 * a + 127) / 255) as u8;
}
std::borrow::Cow::Owned(out)
}