Krolyxon
Commit e3295f9

Branches

Tags

Refs →

remagesrcrenderimage.rs

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