Krolyxon
Commit 74a9b56

Branches

Tags

Refs →

remagesrcrenderimage.rs

490 lines · 16.7 KB · Rust

1//! Image textures: incremental row uploads, GPU mipmaps, linear/nearest sampling, and tiles
2//! for images larger than the GPU's texture limit.
3
4use bytemuck::{Pod, Zeroable};
5use wgpu::util::DeviceExt;
6
7use crate::decode::DecodedImage;
8use crate::state::view::Affine;
9
10const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
11
12#[repr(C)]
13#[derive(Clone, Copy, Pod, Zeroable)]
14struct Uniforms {
15 m: [f32; 4],
16 t: [f32; 2],
17 viewport: [f32; 2],
18 bg: [f32; 4],
19 dot: [f32; 4],
20 params: [f32; 4],
21 /// Gamma, brightness, contrast, and 1 when any differs from neutral.
22 adjust: [f32; 4],
23}
24
25/// How images are painted: the area behind transparent pixels, and display adjustments.
26#[derive(Clone, Copy, Debug)]
27pub struct AlphaStyle {
28 /// Gamma, brightness, contrast.
29 pub adjust: [f32; 3],
30 /// 0 solid, 1 dots, 2 checker.
31 pub mode: u32,
32 pub bg: [f32; 4],
33 pub dot: [f32; 4],
34 pub cell: f32,
35 pub radius: f32,
36}
37
38/// One texture of an image: all of it, or a tile of a huge one.
39struct Tile {
40 texture: wgpu::Texture,
41 bind_group: wgpu::BindGroup,
42 uniforms: wgpu::Buffer,
43 origin: [u32; 2],
44 size: [u32; 2],
45}
46
47pub struct GpuImage {
48 tiles: Vec<Tile>,
49 size: [u32; 2],
50 /// The frame these textures hold (or are being filled with).
51 frame: usize,
52 /// Upload progress: the tile being filled and its rows done.
53 tile: usize,
54 rows_done: u32,
55 ready: bool,
56}
57
58impl GpuImage {
59 pub fn is_ready(&self) -> bool {
60 self.ready
61 }
62
63 pub fn frame(&self) -> usize {
64 self.frame
65 }
66
67 pub fn size(&self) -> [u32; 2] {
68 self.size
69 }
70}
71
72/// Split `size` into tiles no larger than `max` on either side.
73pub fn tiles(size: [u32; 2], max: u32) -> Vec<([u32; 2], [u32; 2])> {
74 let max = max.max(1);
75 let mut out = Vec::new();
76 for y in (0..size[1].max(1)).step_by(max as usize) {
77 for x in (0..size[0].max(1)).step_by(max as usize) {
78 out.push(([x, y], [(size[0] - x).min(max), (size[1] - y).min(max)]));
79 }
80 }
81 out
82}
83
84pub struct ImagePipeline {
85 pipeline: wgpu::RenderPipeline,
86 layout: wgpu::BindGroupLayout,
87 linear: wgpu::BindGroup,
88 nearest: wgpu::BindGroup,
89 mip_pipeline: wgpu::RenderPipeline,
90 mip_layout: wgpu::BindGroupLayout,
91 mip_sampler: wgpu::Sampler,
92}
93
94fn sampler_group(
95 device: &wgpu::Device,
96 layout: &wgpu::BindGroupLayout,
97 filter: wgpu::FilterMode,
98) -> wgpu::BindGroup {
99 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
100 label: Some("image sampler"),
101 mag_filter: filter,
102 min_filter: filter,
103 mipmap_filter: wgpu::MipmapFilterMode::Linear,
104 ..Default::default()
105 });
106 device.create_bind_group(&wgpu::BindGroupDescriptor {
107 label: Some("image sampler"),
108 layout,
109 entries: &[wgpu::BindGroupEntry {
110 binding: 0,
111 resource: wgpu::BindingResource::Sampler(&sampler),
112 }],
113 })
114}
115
116fn texture_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
117 wgpu::BindGroupLayoutEntry {
118 binding,
119 visibility: wgpu::ShaderStages::FRAGMENT,
120 ty: wgpu::BindingType::Texture {
121 sample_type: wgpu::TextureSampleType::Float { filterable: true },
122 view_dimension: wgpu::TextureViewDimension::D2,
123 multisampled: false,
124 },
125 count: None,
126 }
127}
128
129fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
130 wgpu::BindGroupLayoutEntry {
131 binding,
132 visibility: wgpu::ShaderStages::FRAGMENT,
133 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
134 count: None,
135 }
136}
137
138impl ImagePipeline {
139 pub fn new(device: &wgpu::Device, target: wgpu::TextureFormat) -> Self {
140 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
141 label: Some("image"),
142 entries: &[
143 wgpu::BindGroupLayoutEntry {
144 binding: 0,
145 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
146 ty: wgpu::BindingType::Buffer {
147 ty: wgpu::BufferBindingType::Uniform,
148 has_dynamic_offset: false,
149 min_binding_size: None,
150 },
151 count: None,
152 },
153 texture_entry(1),
154 ],
155 });
156 let sampler_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
157 label: Some("image sampler"),
158 entries: &[sampler_entry(0)],
159 });
160 let shader = device.create_shader_module(wgpu::include_wgsl!("shaders/image.wgsl"));
161 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
162 label: Some("image"),
163 bind_group_layouts: &[Some(&layout), Some(&sampler_layout)],
164 immediate_size: 0,
165 });
166 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
167 label: Some("image"),
168 layout: Some(&pipeline_layout),
169 vertex: wgpu::VertexState {
170 module: &shader,
171 entry_point: Some("vs"),
172 compilation_options: Default::default(),
173 buffers: &[],
174 },
175 primitive: wgpu::PrimitiveState::default(),
176 depth_stencil: None,
177 multisample: wgpu::MultisampleState::default(),
178 fragment: Some(wgpu::FragmentState {
179 module: &shader,
180 entry_point: Some("fs"),
181 compilation_options: Default::default(),
182 targets: &[Some(wgpu::ColorTargetState {
183 format: target,
184 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
185 write_mask: wgpu::ColorWrites::ALL,
186 })],
187 }),
188 multiview_mask: None,
189 cache: None,
190 });
191
192 let mip_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
193 label: Some("mip"),
194 entries: &[texture_entry(0), sampler_entry(1)],
195 });
196 let blit = device.create_shader_module(wgpu::include_wgsl!("shaders/blit.wgsl"));
197 let mip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
198 label: Some("mip"),
199 bind_group_layouts: &[Some(&mip_layout)],
200 immediate_size: 0,
201 });
202 let mip_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
203 label: Some("mip"),
204 layout: Some(&mip_pipeline_layout),
205 vertex: wgpu::VertexState {
206 module: &blit,
207 entry_point: Some("vs"),
208 compilation_options: Default::default(),
209 buffers: &[],
210 },
211 primitive: wgpu::PrimitiveState::default(),
212 depth_stencil: None,
213 multisample: wgpu::MultisampleState::default(),
214 fragment: Some(wgpu::FragmentState {
215 module: &blit,
216 entry_point: Some("fs"),
217 compilation_options: Default::default(),
218 targets: &[Some(FORMAT.into())],
219 }),
220 multiview_mask: None,
221 cache: None,
222 });
223 let mip_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
224 label: Some("mip"),
225 mag_filter: wgpu::FilterMode::Linear,
226 min_filter: wgpu::FilterMode::Linear,
227 ..Default::default()
228 });
229
230 Self {
231 pipeline,
232 linear: sampler_group(device, &sampler_layout, wgpu::FilterMode::Linear),
233 nearest: sampler_group(device, &sampler_layout, wgpu::FilterMode::Nearest),
234 layout,
235 mip_pipeline,
236 mip_layout,
237 mip_sampler,
238 }
239 }
240
241 /// Allocate textures for `img`, tiled at `max_dim`; fill them with [`ImagePipeline::upload`].
242 pub fn create(&self, device: &wgpu::Device, img: &DecodedImage, max_dim: u32) -> GpuImage {
243 let tiles = tiles(img.size, max_dim)
244 .into_iter()
245 .map(|(origin, size)| self.tile(device, origin, size))
246 .collect::<Vec<_>>();
247 if tiles.len() > 1 {
248 log::debug!(
249 "{}×{} image in {} tiles",
250 img.size[0],
251 img.size[1],
252 tiles.len()
253 );
254 }
255 GpuImage {
256 tiles,
257 size: img.size,
258 frame: 0,
259 tile: 0,
260 rows_done: 0,
261 ready: false,
262 }
263 }
264
265 fn tile(&self, device: &wgpu::Device, origin: [u32; 2], [w, h]: [u32; 2]) -> Tile {
266 let mips = 32 - w.max(h).max(1).leading_zeros();
267 let texture = device.create_texture(&wgpu::TextureDescriptor {
268 label: Some("image"),
269 size: wgpu::Extent3d {
270 width: w,
271 height: h,
272 depth_or_array_layers: 1,
273 },
274 mip_level_count: mips,
275 sample_count: 1,
276 dimension: wgpu::TextureDimension::D2,
277 format: FORMAT,
278 usage: wgpu::TextureUsages::TEXTURE_BINDING
279 | wgpu::TextureUsages::COPY_DST
280 | wgpu::TextureUsages::RENDER_ATTACHMENT,
281 view_formats: &[],
282 });
283 let uniforms = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
284 label: Some("image uniforms"),
285 contents: bytemuck::bytes_of(&Uniforms::zeroed()),
286 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
287 });
288 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
289 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
290 label: Some("image"),
291 layout: &self.layout,
292 entries: &[
293 wgpu::BindGroupEntry {
294 binding: 0,
295 resource: uniforms.as_entire_binding(),
296 },
297 wgpu::BindGroupEntry {
298 binding: 1,
299 resource: wgpu::BindingResource::TextureView(&view),
300 },
301 ],
302 });
303 Tile {
304 texture,
305 bind_group,
306 uniforms,
307 origin,
308 size: [w, h],
309 }
310 }
311
312 /// Start filling the textures with another frame; finish with [`ImagePipeline::upload`].
313 pub fn restart(&self, gpu: &mut GpuImage, frame: usize) {
314 gpu.frame = frame;
315 gpu.tile = 0;
316 gpu.rows_done = 0;
317 gpu.ready = false;
318 }
319
320 /// Upload up to `budget` bytes of rows. Returns true once the image is complete.
321 pub fn upload(
322 &self,
323 device: &wgpu::Device,
324 queue: &wgpu::Queue,
325 gpu: &mut GpuImage,
326 img: &DecodedImage,
327 budget: usize,
328 ) -> bool {
329 let pixels = img.frame(gpu.frame);
330 let stride = img.size[0] as usize * 4;
331 let mut left = budget;
332 while !gpu.ready {
333 let tile = &gpu.tiles[gpu.tile];
334 let [w, h] = tile.size;
335 let row_bytes = w as usize * 4;
336 let rows = ((left / row_bytes.max(1)) as u32).clamp(1, h - gpu.rows_done);
337 let y = tile.origin[1] + gpu.rows_done;
338 let start = y as usize * stride + tile.origin[0] as usize * 4;
339 let end = start + (rows as usize - 1) * stride + row_bytes;
340 queue.write_texture(
341 wgpu::TexelCopyTextureInfo {
342 texture: &tile.texture,
343 mip_level: 0,
344 origin: wgpu::Origin3d {
345 x: 0,
346 y: gpu.rows_done,
347 z: 0,
348 },
349 aspect: wgpu::TextureAspect::All,
350 },
351 &pixels[start..end],
352 wgpu::TexelCopyBufferLayout {
353 offset: 0,
354 bytes_per_row: Some(stride as u32),
355 rows_per_image: Some(rows),
356 },
357 wgpu::Extent3d {
358 width: w,
359 height: rows,
360 depth_or_array_layers: 1,
361 },
362 );
363 gpu.rows_done += rows;
364 left = left.saturating_sub(rows as usize * row_bytes);
365 if gpu.rows_done == h {
366 self.generate_mips(device, queue, &gpu.tiles[gpu.tile].texture);
367 gpu.tile += 1;
368 gpu.rows_done = 0;
369 gpu.ready = gpu.tile == gpu.tiles.len();
370 }
371 if left == 0 {
372 break;
373 }
374 }
375 if !gpu.ready {
376 queue.submit([]);
377 }
378 gpu.ready
379 }
380
381 fn generate_mips(&self, device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture) {
382 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
383 label: Some("mips"),
384 });
385 let level_view = |level| {
386 texture.create_view(&wgpu::TextureViewDescriptor {
387 base_mip_level: level,
388 mip_level_count: Some(1),
389 ..Default::default()
390 })
391 };
392 for level in 1..texture.mip_level_count() {
393 let src = level_view(level - 1);
394 let dst = level_view(level);
395 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
396 label: Some("mip"),
397 layout: &self.mip_layout,
398 entries: &[
399 wgpu::BindGroupEntry {
400 binding: 0,
401 resource: wgpu::BindingResource::TextureView(&src),
402 },
403 wgpu::BindGroupEntry {
404 binding: 1,
405 resource: wgpu::BindingResource::Sampler(&self.mip_sampler),
406 },
407 ],
408 });
409 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
410 label: Some("mip"),
411 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
412 view: &dst,
413 depth_slice: None,
414 resolve_target: None,
415 ops: wgpu::Operations {
416 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
417 store: wgpu::StoreOp::Store,
418 },
419 })],
420 depth_stencil_attachment: None,
421 timestamp_writes: None,
422 occlusion_query_set: None,
423 multiview_mask: None,
424 });
425 pass.set_pipeline(&self.mip_pipeline);
426 pass.set_bind_group(0, &bind_group, &[]);
427 pass.draw(0..3, 0..1);
428 }
429 queue.submit([encoder.finish()]);
430 }
431
432 /// Write this frame's placement; call before the render pass begins.
433 pub fn prepare(
434 &self,
435 queue: &wgpu::Queue,
436 gpu: &GpuImage,
437 affine: Affine,
438 viewport: [f32; 2],
439 alpha: f32,
440 style: &AlphaStyle,
441 ) {
442 let adjust = style.adjust;
443 let on = adjust != [1.0, 0.0, 1.0];
444 let [iw, ih] = [gpu.size[0] as f32, gpu.size[1] as f32];
445 let m = affine.m;
446 for tile in &gpu.tiles {
447 // The tile's corner and extent in the image's 0..1 coordinates.
448 let (u0, v0) = (tile.origin[0] as f32 / iw, tile.origin[1] as f32 / ih);
449 let (du, dv) = (tile.size[0] as f32 / iw, tile.size[1] as f32 / ih);
450 let u = Uniforms {
451 m: [m[0] * du, m[1] * dv, m[2] * du, m[3] * dv],
452 t: [
453 affine.t[0] + m[0] * u0 + m[1] * v0,
454 affine.t[1] + m[2] * u0 + m[3] * v0,
455 ],
456 viewport,
457 bg: style.bg,
458 dot: style.dot,
459 params: [alpha, style.mode as f32, style.cell, style.radius],
460 adjust: [adjust[0], adjust[1], adjust[2], on as u32 as f32],
461 };
462 queue.write_buffer(&tile.uniforms, 0, bytemuck::bytes_of(&u));
463 }
464 }
465
466 pub fn draw(&self, pass: &mut wgpu::RenderPass<'_>, gpu: &GpuImage, nearest: bool) {
467 pass.set_pipeline(&self.pipeline);
468 pass.set_bind_group(1, if nearest { &self.nearest } else { &self.linear }, &[]);
469 for tile in &gpu.tiles {
470 pass.set_bind_group(0, &tile.bind_group, &[]);
471 pass.draw(0..6, 0..1);
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn tiling_covers_the_image() {
482 assert_eq!(tiles([10, 5], 16), [([0, 0], [10, 5])]);
483 let t = tiles([10, 5], 4);
484 assert_eq!(t.len(), 6);
485 assert_eq!(t[2], ([8, 0], [2, 4]));
486 assert_eq!(t[5], ([8, 4], [2, 1]));
487 let area: u32 = t.iter().map(|(_, s)| s[0] * s[1]).sum();
488 assert_eq!(area, 50);
489 }
490}