Krolyxon
Commit ab1dc3b

Branches

Tags

Refs →

remagesrcrendermod.rs

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