Krolyxon
Commit e3295f9

Branches

Tags

Refs →

remagesrcrendermod.rs

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