Krolyxon
Commit 74a9b56

Branches

Tags

Refs →

remagesrcrendermod.rs

438 lines · 15.7 KB · Rust

1//! wgpu device and surface setup, and per-frame orchestration.
2
3pub mod atlas;
4pub mod image;
5pub mod prims;
6pub mod text;
7
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use anyhow::Context;
13use winit::window::Window;
14
15use crate::config::theme::Rgba;
16use crate::decode::DecodedImage;
17use crate::state::view::Affine;
18use crate::thumbs::Thumb;
19use atlas::{Atlas, ThumbDraw};
20use image::{AlphaStyle, GpuImage, ImagePipeline};
21use prims::{Prim, PrimPipeline};
22use text::{TextItem, TextSystem};
23
24pub struct ImageDraw<'a> {
25 pub path: &'a Path,
26 pub affine: Affine,
27 pub alpha: f32,
28 pub nearest: bool,
29}
30
31/// Everything one frame shows, in physical pixels. Built by `ui`, drawn here.
32pub struct Scene<'a> {
33 pub clear: Rgba,
34 /// Images are clipped to this rect (x, y, w, h): the area above the status bar.
35 pub image_clip: [f32; 4],
36 pub alpha: AlphaStyle,
37 pub images: Vec<ImageDraw<'a>>,
38 /// Grid thumbnails, clipped like images.
39 pub thumbs: Vec<ThumbDraw<'a>>,
40 /// Drawn before images and thumbnails (clipped like them), e.g. selection backgrounds.
41 pub under: Vec<Prim>,
42 pub prims: Vec<Prim>,
43 pub texts: Vec<TextItem>,
44 /// Panels (palette, help, info, which-key) and their text, above everything else.
45 pub overlay_prims: Vec<Prim>,
46 pub overlay_texts: Vec<TextItem>,
47}
48
49pub struct Renderer {
50 window: Arc<Window>,
51 instance: wgpu::Instance,
52 surface: wgpu::Surface<'static>,
53 device: wgpu::Device,
54 queue: wgpu::Queue,
55 config: wgpu::SurfaceConfiguration,
56 view_format: wgpu::TextureFormat,
57 images: ImagePipeline,
58 prims: PrimPipeline,
59 atlas: Atlas,
60 text: TextSystem,
61 textures: HashMap<PathBuf, GpuImage>,
62 max_dim: u32,
63}
64
65impl Renderer {
66 pub fn new(
67 window: Arc<Window>,
68 display: winit::event_loop::OwnedDisplayHandle,
69 ) -> anyhow::Result<Self> {
70 let mut desc =
71 wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display));
72 if wgpu::Backends::from_env().is_none() {
73 desc.backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
74 }
75 let instance = wgpu::Instance::new(desc);
76 let surface = instance
77 .create_surface(window.clone())
78 .context("create surface")?;
79 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
80 // The integrated GPU is plenty for images and doesn't wake a sleeping dGPU.
81 power_preference:
82 wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower),
83 compatible_surface: Some(&surface),
84 ..Default::default()
85 }))
86 .context("no suitable GPU adapter")?;
87 let info = adapter.get_info();
88 log::debug!("adapter: {} ({:?})", info.name, info.backend);
89 let limits = wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits());
90 // `REMAGE_MAX_TEXTURE` lowers the limit, to exercise tiling on any GPU.
91 let max_dim = std::env::var("REMAGE_MAX_TEXTURE")
92 .ok()
93 .and_then(|v| v.parse::<u32>().ok())
94 .filter(|v| *v >= 64)
95 .map_or(limits.max_texture_dimension_2d, |v| {
96 v.min(limits.max_texture_dimension_2d)
97 });
98 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
99 label: Some("remage"),
100 required_limits: limits,
101 ..Default::default()
102 }))
103 .context("request GPU device")?;
104
105 let caps = surface.get_capabilities(&adapter);
106 let format = caps
107 .formats
108 .iter()
109 .copied()
110 .find(|f| f.is_srgb() || f.add_srgb_suffix() != *f)
111 .or_else(|| caps.formats.first().copied())
112 .context("surface supports no formats")?;
113 // Render through an sRGB view so blending happens in linear light.
114 let view_format = format.add_srgb_suffix();
115 let alpha_mode = if caps.alpha_modes.contains(&wgpu::CompositeAlphaMode::Opaque) {
116 wgpu::CompositeAlphaMode::Opaque
117 } else {
118 caps.alpha_modes
119 .first()
120 .copied()
121 .unwrap_or(wgpu::CompositeAlphaMode::Auto)
122 };
123 let size = window.inner_size();
124 let config = wgpu::SurfaceConfiguration {
125 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
126 format,
127 width: size.width.max(1),
128 height: size.height.max(1),
129 present_mode: wgpu::PresentMode::Fifo,
130 alpha_mode,
131 view_formats: if view_format == format {
132 vec![]
133 } else {
134 vec![view_format]
135 },
136 desired_maximum_frame_latency: 1,
137 color_space: wgpu::SurfaceColorSpace::Auto,
138 };
139 surface.configure(&device, &config);
140
141 Ok(Self {
142 images: ImagePipeline::new(&device, view_format),
143 prims: PrimPipeline::new(&device, view_format),
144 atlas: Atlas::new(&device, view_format, max_dim),
145 text: TextSystem::new(&device, &queue, view_format),
146 window,
147 instance,
148 surface,
149 device,
150 queue,
151 config,
152 view_format,
153 textures: HashMap::new(),
154 max_dim,
155 })
156 }
157
158 pub fn resize(&mut self, width: u32, height: u32) {
159 if width == 0 || height == 0 {
160 return;
161 }
162 self.config.width = width;
163 self.config.height = height;
164 self.surface.configure(&self.device, &self.config);
165 }
166
167 pub fn measure(&mut self, item: &TextItem) -> f32 {
168 self.text.measure(item)
169 }
170
171 pub fn set_font_families(&mut self, families: [String; 3]) {
172 self.text.set_families(families);
173 }
174
175 pub fn set_fonts(&mut self, db: glyphon::fontdb::Database) {
176 self.text.set_fonts(&self.device, &self.queue, db);
177 }
178
179 pub fn is_ready(&self, path: &Path) -> bool {
180 self.textures.get(path).is_some_and(GpuImage::is_ready)
181 }
182
183 /// Upload up to `budget` bytes of `img`. Returns true once it is ready to draw.
184 /// A texture of another size (a re-rasterised SVG, a developed RAW) is replaced.
185 pub fn upload(&mut self, path: &Path, img: &DecodedImage, budget: usize) -> bool {
186 if self
187 .textures
188 .get(path)
189 .is_some_and(|g| g.size() != img.size)
190 {
191 self.textures.remove(path);
192 }
193 let gpu = match self.textures.get_mut(path) {
194 Some(g) => g,
195 None => {
196 let g = self.images.create(&self.device, img, self.max_dim);
197 self.textures.entry(path.to_path_buf()).or_insert(g)
198 }
199 };
200 self.images
201 .upload(&self.device, &self.queue, gpu, img, budget)
202 }
203
204 /// The frame an image's textures hold.
205 pub fn frame(&self, path: &Path) -> Option<usize> {
206 self.textures.get(path).map(GpuImage::frame)
207 }
208
209 /// Replace the textures' contents with frame `frame` of `img`, in full.
210 pub fn show_frame(&mut self, path: &Path, img: &DecodedImage, frame: usize) {
211 let Some(gpu) = self.textures.get_mut(path) else {
212 return;
213 };
214 if gpu.size() != img.size {
215 return;
216 }
217 self.images.restart(gpu, frame);
218 self.images
219 .upload(&self.device, &self.queue, gpu, img, usize::MAX);
220 }
221
222 /// Start a fresh thumbnail atlas for `cell`-pixel thumbnails.
223 pub fn reset_thumbs(&mut self, cell: u32) {
224 self.atlas.reset(cell);
225 }
226
227 pub fn has_thumb(&self, path: &Path) -> bool {
228 self.atlas.contains(path)
229 }
230
231 /// Put a thumbnail in the atlas. Returns the paths evicted to make room.
232 pub fn insert_thumb(
233 &mut self,
234 path: &Path,
235 thumb: &Thumb,
236 wanted: impl Fn(&Path) -> bool,
237 ) -> Vec<PathBuf> {
238 self.atlas
239 .insert(&self.device, &self.queue, path, thumb, wanted)
240 }
241
242 /// Drop textures that are no longer wanted.
243 pub fn retain(&mut self, keep: impl Fn(&Path) -> bool) {
244 self.textures.retain(|p, _| keep(p));
245 }
246
247 pub fn render(&mut self, scene: &Scene) -> anyhow::Result<()> {
248 let frame = match self.surface.get_current_texture() {
249 wgpu::CurrentSurfaceTexture::Success(f) => f,
250 wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
251 self.surface.configure(&self.device, &self.config);
252 f
253 }
254 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
255 return Ok(());
256 }
257 wgpu::CurrentSurfaceTexture::Outdated => {
258 self.surface.configure(&self.device, &self.config);
259 self.window.request_redraw();
260 return Ok(());
261 }
262 wgpu::CurrentSurfaceTexture::Lost => {
263 self.surface = self.instance.create_surface(self.window.clone())?;
264 self.surface.configure(&self.device, &self.config);
265 self.window.request_redraw();
266 return Ok(());
267 }
268 wgpu::CurrentSurfaceTexture::Validation => anyhow::bail!("surface validation error"),
269 };
270 let size = [self.config.width, self.config.height];
271 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
272 format: Some(self.view_format),
273 ..Default::default()
274 });
275 let commands = self.encode(scene, &view, size)?;
276 self.queue.submit([commands]);
277 self.window.pre_present_notify();
278 self.queue.present(frame);
279 self.text.trim();
280 Ok(())
281 }
282
283 /// Record the frame into `view`. Uniform and text uploads happen here too.
284 fn encode(
285 &mut self,
286 scene: &Scene,
287 view: &wgpu::TextureView,
288 size: [u32; 2],
289 ) -> anyhow::Result<wgpu::CommandBuffer> {
290 let viewport = [size[0] as f32, size[1] as f32];
291
292 let draws: Vec<_> = scene
293 .images
294 .iter()
295 .filter_map(|d| {
296 self.textures
297 .get(d.path)
298 .filter(|g| g.is_ready())
299 .map(|g| (d, g))
300 })
301 .collect();
302 for (d, gpu) in &draws {
303 self.images
304 .prepare(&self.queue, gpu, d.affine, viewport, d.alpha, &scene.alpha);
305 }
306 self.atlas
307 .prepare(&self.device, &self.queue, &scene.thumbs, viewport);
308 self.prims.prepare(
309 &self.device,
310 &self.queue,
311 [&scene.under, &scene.prims, &scene.overlay_prims],
312 viewport,
313 );
314 self.text.prepare(
315 &self.device,
316 &self.queue,
317 [&scene.texts, &scene.overlay_texts],
318 size,
319 )?;
320
321 let mut encoder = self
322 .device
323 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
324 label: Some("frame"),
325 });
326 {
327 let [r, g, b, a] = scene.clear.linear();
328 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
329 label: Some("frame"),
330 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
331 view,
332 depth_slice: None,
333 resolve_target: None,
334 ops: wgpu::Operations {
335 load: wgpu::LoadOp::Clear(wgpu::Color {
336 r: r as f64,
337 g: g as f64,
338 b: b as f64,
339 a: a as f64,
340 }),
341 store: wgpu::StoreOp::Store,
342 },
343 })],
344 depth_stencil_attachment: None,
345 timestamp_writes: None,
346 occlusion_query_set: None,
347 multiview_mask: None,
348 });
349 if let Some((x, y, w, h)) = clip(scene.image_clip, size) {
350 pass.set_scissor_rect(x, y, w, h);
351 self.prims.draw_under(&mut pass);
352 for (d, gpu) in &draws {
353 self.images.draw(&mut pass, gpu, d.nearest);
354 }
355 self.atlas.draw(&mut pass);
356 pass.set_scissor_rect(0, 0, size[0], size[1]);
357 }
358 self.prims.draw_over(&mut pass);
359 self.text.render(&mut pass)?;
360 self.prims.draw_overlay(&mut pass);
361 self.text.render_overlay(&mut pass)?;
362 }
363 Ok(encoder.finish())
364 }
365
366 /// Debug aid (`REMAGE_SNAPSHOT=out.png`): render `scene` offscreen and save it.
367 pub fn snapshot(&mut self, scene: &Scene, path: &Path) -> anyhow::Result<()> {
368 let size = [self.config.width, self.config.height];
369 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
370 label: Some("snapshot"),
371 size: wgpu::Extent3d {
372 width: size[0],
373 height: size[1],
374 depth_or_array_layers: 1,
375 },
376 mip_level_count: 1,
377 sample_count: 1,
378 dimension: wgpu::TextureDimension::D2,
379 format: self.view_format,
380 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
381 view_formats: &[],
382 });
383 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
384 let commands = self.encode(scene, &view, size)?;
385 let row = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
386 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
387 label: Some("snapshot"),
388 size: row as u64 * size[1] as u64,
389 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
390 mapped_at_creation: false,
391 });
392 let mut encoder = self
393 .device
394 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
395 encoder.copy_texture_to_buffer(
396 texture.as_image_copy(),
397 wgpu::TexelCopyBufferInfo {
398 buffer: &buffer,
399 layout: wgpu::TexelCopyBufferLayout {
400 offset: 0,
401 bytes_per_row: Some(row),
402 rows_per_image: Some(size[1]),
403 },
404 },
405 texture.size(),
406 );
407 self.queue.submit([commands, encoder.finish()]);
408 buffer.map_async(wgpu::MapMode::Read, .., |_| {});
409 self.device.poll(wgpu::PollType::wait_indefinitely())?;
410 let data = buffer.get_mapped_range(..)?;
411 let bgra = matches!(self.view_format, wgpu::TextureFormat::Bgra8UnormSrgb);
412 let mut pixels = Vec::with_capacity((size[0] * size[1] * 4) as usize);
413 for y in 0..size[1] as usize {
414 let line = &data[y * row as usize..][..size[0] as usize * 4];
415 for px in line.as_chunks::<4>().0.iter() {
416 let rgb = if bgra {
417 [px[2], px[1], px[0]]
418 } else {
419 [px[0], px[1], px[2]]
420 };
421 pixels.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
422 }
423 }
424 ::image::RgbaImage::from_raw(size[0], size[1], pixels)
425 .context("snapshot buffer size")?
426 .save(path)?;
427 Ok(())
428 }
429}
430
431/// Integer scissor rect inside the surface, or `None` if empty.
432fn clip(r: [f32; 4], size: [u32; 2]) -> Option<(u32, u32, u32, u32)> {
433 let x0 = r[0].max(0.0).round() as u32;
434 let y0 = r[1].max(0.0).round() as u32;
435 let x1 = ((r[0] + r[2]).round() as u32).min(size[0]);
436 let y1 = ((r[1] + r[3]).round() as u32).min(size[1]);
437 (x1 > x0 && y1 > y0).then(|| (x0, y0, x1 - x0, y1 - y0))
438}