Krolyxon
Tag v0.1.1

Branches

Tags

Refs →

remagesrcrendermod.rs

475 lines · 17.3 KB · Rust

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