Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcrendermod.rs

359 lines · 13.0 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_fonts(&mut self, db: glyphon::fontdb::Database) {
157 self.text.set_fonts(&self.device, &self.queue, db);
158 }
159
160 pub fn is_ready(&self, path: &Path) -> bool {
161 self.textures.get(path).is_some_and(GpuImage::is_ready)
162 }
163
164 /// Upload up to `budget` bytes of `img`. Returns true once it is ready to draw.
165 pub fn upload(&mut self, path: &Path, img: &DecodedImage, budget: usize) -> bool {
166 let gpu = match self.textures.get_mut(path) {
167 Some(g) => g,
168 None => {
169 let g = self.images.create(&self.device, img);
170 self.textures.entry(path.to_path_buf()).or_insert(g)
171 }
172 };
173 self.images
174 .upload(&self.device, &self.queue, gpu, img, budget)
175 }
176
177 /// Drop textures that are no longer wanted.
178 pub fn retain(&mut self, keep: impl Fn(&Path) -> bool) {
179 self.textures.retain(|p, _| keep(p));
180 }
181
182 pub fn render(&mut self, scene: &Scene) -> anyhow::Result<()> {
183 let frame = match self.surface.get_current_texture() {
184 wgpu::CurrentSurfaceTexture::Success(f) => f,
185 wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
186 self.surface.configure(&self.device, &self.config);
187 f
188 }
189 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
190 return Ok(());
191 }
192 wgpu::CurrentSurfaceTexture::Outdated => {
193 self.surface.configure(&self.device, &self.config);
194 self.window.request_redraw();
195 return Ok(());
196 }
197 wgpu::CurrentSurfaceTexture::Lost => {
198 self.surface = self.instance.create_surface(self.window.clone())?;
199 self.surface.configure(&self.device, &self.config);
200 self.window.request_redraw();
201 return Ok(());
202 }
203 wgpu::CurrentSurfaceTexture::Validation => anyhow::bail!("surface validation error"),
204 };
205 let size = [self.config.width, self.config.height];
206 let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
207 format: Some(self.view_format),
208 ..Default::default()
209 });
210 let commands = self.encode(scene, &view, size)?;
211 self.queue.submit([commands]);
212 self.window.pre_present_notify();
213 self.queue.present(frame);
214 self.text.trim();
215 Ok(())
216 }
217
218 /// Record the frame into `view`. Uniform and text uploads happen here too.
219 fn encode(
220 &mut self,
221 scene: &Scene,
222 view: &wgpu::TextureView,
223 size: [u32; 2],
224 ) -> anyhow::Result<wgpu::CommandBuffer> {
225 let viewport = [size[0] as f32, size[1] as f32];
226
227 let draws: Vec<_> = scene
228 .images
229 .iter()
230 .filter_map(|d| {
231 self.textures
232 .get(d.path)
233 .filter(|g| g.is_ready())
234 .map(|g| (d, g))
235 })
236 .collect();
237 for (d, gpu) in &draws {
238 self.images
239 .prepare(&self.queue, gpu, d.affine, viewport, d.alpha, &scene.alpha);
240 }
241 self.prims
242 .prepare(&self.device, &self.queue, &scene.prims, viewport);
243 self.text
244 .prepare(&self.device, &self.queue, &scene.texts, size)?;
245
246 let mut encoder = self
247 .device
248 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
249 label: Some("frame"),
250 });
251 {
252 let [r, g, b, a] = scene.clear.linear();
253 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
254 label: Some("frame"),
255 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
256 view,
257 depth_slice: None,
258 resolve_target: None,
259 ops: wgpu::Operations {
260 load: wgpu::LoadOp::Clear(wgpu::Color {
261 r: r as f64,
262 g: g as f64,
263 b: b as f64,
264 a: a as f64,
265 }),
266 store: wgpu::StoreOp::Store,
267 },
268 })],
269 depth_stencil_attachment: None,
270 timestamp_writes: None,
271 occlusion_query_set: None,
272 multiview_mask: None,
273 });
274 if let Some((x, y, w, h)) = clip(scene.image_clip, size) {
275 pass.set_scissor_rect(x, y, w, h);
276 for (d, gpu) in &draws {
277 self.images.draw(&mut pass, gpu, d.nearest);
278 }
279 pass.set_scissor_rect(0, 0, size[0], size[1]);
280 }
281 self.prims.draw(&mut pass);
282 self.text.render(&mut pass)?;
283 }
284 Ok(encoder.finish())
285 }
286
287 /// Debug aid (`REMAGE_SNAPSHOT=out.png`): render `scene` offscreen and save it.
288 pub fn snapshot(&mut self, scene: &Scene, path: &Path) -> anyhow::Result<()> {
289 let size = [self.config.width, self.config.height];
290 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
291 label: Some("snapshot"),
292 size: wgpu::Extent3d {
293 width: size[0],
294 height: size[1],
295 depth_or_array_layers: 1,
296 },
297 mip_level_count: 1,
298 sample_count: 1,
299 dimension: wgpu::TextureDimension::D2,
300 format: self.view_format,
301 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
302 view_formats: &[],
303 });
304 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
305 let commands = self.encode(scene, &view, size)?;
306 let row = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
307 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
308 label: Some("snapshot"),
309 size: row as u64 * size[1] as u64,
310 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
311 mapped_at_creation: false,
312 });
313 let mut encoder = self
314 .device
315 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
316 encoder.copy_texture_to_buffer(
317 texture.as_image_copy(),
318 wgpu::TexelCopyBufferInfo {
319 buffer: &buffer,
320 layout: wgpu::TexelCopyBufferLayout {
321 offset: 0,
322 bytes_per_row: Some(row),
323 rows_per_image: Some(size[1]),
324 },
325 },
326 texture.size(),
327 );
328 self.queue.submit([commands, encoder.finish()]);
329 buffer.map_async(wgpu::MapMode::Read, .., |_| {});
330 self.device.poll(wgpu::PollType::wait_indefinitely())?;
331 let data = buffer.get_mapped_range(..)?;
332 let bgra = matches!(self.view_format, wgpu::TextureFormat::Bgra8UnormSrgb);
333 let mut pixels = Vec::with_capacity((size[0] * size[1] * 4) as usize);
334 for y in 0..size[1] as usize {
335 let line = &data[y * row as usize..][..size[0] as usize * 4];
336 for px in line.chunks_exact(4) {
337 let rgb = if bgra {
338 [px[2], px[1], px[0]]
339 } else {
340 [px[0], px[1], px[2]]
341 };
342 pixels.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
343 }
344 }
345 ::image::RgbaImage::from_raw(size[0], size[1], pixels)
346 .context("snapshot buffer size")?
347 .save(path)?;
348 Ok(())
349 }
350}
351
352/// Integer scissor rect inside the surface, or `None` if empty.
353fn clip(r: [f32; 4], size: [u32; 2]) -> Option<(u32, u32, u32, u32)> {
354 let x0 = r[0].max(0.0).round() as u32;
355 let y0 = r[1].max(0.0).round() as u32;
356 let x1 = ((r[0] + r[2]).round() as u32).min(size[0]);
357 let y1 = ((r[1] + r[3]).round() as u32).min(size[1]);
358 (x1 > x0 && y1 > y0).then(|| (x0, y0, x1 - x0, y1 - y0))
359}