Krolyxon
Commit 92928dd

Branches

Tags

Refs →

remagesrcrendermod.rs

363 lines · 13.1 KB · Rust

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