Skip to main content

tessera_ui/
graphics.rs

1//! Renderer-owned graphics resources and upload observations.
2//!
3//! ## Usage
4//!
5//! Reuse pipeline buffers and inspect actual resource allocations and uploads.
6
7use std::{
8    ops::Deref,
9    sync::{
10        Arc,
11        atomic::{AtomicBool, AtomicU64, Ordering},
12    },
13};
14
15use parking_lot::Mutex;
16use rustc_hash::FxHashMap;
17use wgpu::util::DeviceExt as _;
18
19pub use self::{
20    frame::FrameBuffer,
21    scratch::{ScratchFit, ScratchTexture, ScratchTextureDescriptor},
22};
23
24mod frame;
25mod scratch;
26
27/// Actual resource operations for a pipeline's declared resource group.
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29#[cfg_attr(feature = "headless", derive(serde::Serialize))]
30pub struct GraphicsWork {
31    /// Number of buffers created.
32    pub buffer_allocations: usize,
33    /// Number of queue buffer writes.
34    pub buffer_uploads: usize,
35    /// Bytes submitted through queue buffer writes.
36    pub buffer_uploaded_bytes: u64,
37    /// Number of textures created.
38    pub texture_allocations: usize,
39    /// Number of queue texture writes.
40    pub texture_uploads: usize,
41    /// Bytes submitted through queue texture writes.
42    pub texture_uploaded_bytes: u64,
43}
44
45/// Cumulative graphics work owned by one renderer.
46#[derive(Clone, Debug, Default)]
47#[cfg_attr(feature = "headless", derive(serde::Serialize))]
48pub struct GraphicsWorkDiagnostics {
49    #[cfg_attr(feature = "headless", serde(skip))]
50    owner: u128,
51    groups: FxHashMap<&'static str, GraphicsWork>,
52}
53
54impl GraphicsWorkDiagnostics {
55    /// Returns operations after an earlier snapshot from the same renderer.
56    pub fn since(&self, earlier: &Self) -> Self {
57        assert_eq!(
58            self.owner, earlier.owner,
59            "graphics snapshots belong to different renderers"
60        );
61        assert!(
62            earlier
63                .groups
64                .keys()
65                .all(|name| self.groups.contains_key(name)),
66            "graphics baseline is newer than snapshot"
67        );
68        let mut difference = self.clone();
69        for (name, value) in &mut difference.groups {
70            let before = earlier.group(name);
71            value.buffer_allocations = value
72                .buffer_allocations
73                .checked_sub(before.buffer_allocations)
74                .expect("graphics baseline is newer than snapshot");
75            value.buffer_uploads = value
76                .buffer_uploads
77                .checked_sub(before.buffer_uploads)
78                .expect("graphics baseline is newer than snapshot");
79            value.buffer_uploaded_bytes = value
80                .buffer_uploaded_bytes
81                .checked_sub(before.buffer_uploaded_bytes)
82                .expect("graphics baseline is newer than snapshot");
83            value.texture_allocations = value
84                .texture_allocations
85                .checked_sub(before.texture_allocations)
86                .expect("graphics baseline is newer than snapshot");
87            value.texture_uploads = value
88                .texture_uploads
89                .checked_sub(before.texture_uploads)
90                .expect("graphics baseline is newer than snapshot");
91            value.texture_uploaded_bytes = value
92                .texture_uploaded_bytes
93                .checked_sub(before.texture_uploaded_bytes)
94                .expect("graphics baseline is newer than snapshot");
95        }
96        difference
97    }
98    /// Returns operations for a declared pipeline resource group.
99    pub fn group(&self, group: &str) -> GraphicsWork {
100        self.groups.get(group).copied().unwrap_or_default()
101    }
102}
103
104/// A graphics service bound to one renderer's device and upload queue.
105#[derive(Clone, Debug)]
106pub struct GraphicsDevice {
107    device: wgpu::Device,
108    queue: wgpu::Queue,
109    work: Arc<Mutex<GraphicsWorkDiagnostics>>,
110    frame_uploads: Arc<FrameUploads>,
111    prelude: Arc<Mutex<Option<wgpu::CommandEncoder>>>,
112    scratch: Arc<Mutex<scratch::ScratchPool>>,
113    frame_resources: Arc<Mutex<frame::FrameResources>>,
114}
115
116#[derive(Debug, Default)]
117struct FrameUploads {
118    bytes: AtomicU64,
119    deferred: AtomicBool,
120}
121
122impl GraphicsDevice {
123    pub(crate) fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
124        Self {
125            device,
126            queue,
127            work: Arc::new(Mutex::new(GraphicsWorkDiagnostics {
128                owner: uuid::Uuid::new_v4().as_u128(),
129                groups: FxHashMap::default(),
130            })),
131            frame_uploads: Arc::default(),
132            prelude: Arc::default(),
133            scratch: Arc::default(),
134            frame_resources: Arc::default(),
135        }
136    }
137
138    /// Records GPU work that runs before the current frame's passes, in the
139    /// same submission.
140    ///
141    /// Use it to prepare textures, such as atlas entries, that the frame
142    /// samples. Queue writes made during the frame complete before this work.
143    pub fn encode_before_frame(&self, record: impl FnOnce(&mut wgpu::CommandEncoder)) {
144        let mut prelude = self.prelude.lock();
145        record(prelude.get_or_insert_with(|| {
146            self.device
147                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
148                    label: Some("Frame Prelude"),
149                })
150        }));
151    }
152
153    pub(crate) fn finish_prelude(&self) -> Option<wgpu::CommandBuffer> {
154        self.prelude.lock().take().map(wgpu::CommandEncoder::finish)
155    }
156
157    /// Returns the device used by this renderer.
158    pub fn device(&self) -> &wgpu::Device {
159        &self.device
160    }
161
162    /// Returns the upload queue used by this renderer.
163    pub fn queue(&self) -> &wgpu::Queue {
164        &self.queue
165    }
166
167    /// Texture bytes that can still be uploaded in the current frame.
168    ///
169    /// Spread larger uploads over several frames, and call
170    /// [`Self::defer_upload`] while some remain.
171    pub fn upload_budget(&self) -> u64 {
172        FRAME_UPLOAD_BUDGET.saturating_sub(self.frame_uploads.bytes.load(Ordering::Relaxed))
173    }
174
175    /// Requests another frame to continue uploads deferred by the budget.
176    pub fn defer_upload(&self) {
177        self.frame_uploads.deferred.store(true, Ordering::Relaxed);
178    }
179
180    pub(crate) fn begin_frame(&self) {
181        self.frame_uploads.bytes.store(0, Ordering::Relaxed);
182        self.scratch.lock().begin_frame();
183        self.frame_resources.lock().begin_frame();
184    }
185
186    pub(crate) fn take_deferred_upload(&self) -> bool {
187        self.frame_uploads.deferred.swap(false, Ordering::Relaxed)
188    }
189
190    /// Captures actual operations performed through this graphics service.
191    pub fn diagnostics(&self) -> GraphicsWorkDiagnostics {
192        self.work.lock().clone()
193    }
194
195    /// Creates an owned buffer in a pipeline resource group.
196    pub fn create_buffer(
197        &self,
198        group: &'static str,
199        descriptor: &wgpu::BufferDescriptor<'_>,
200    ) -> TrackedBuffer {
201        let buffer = self.device.create_buffer(descriptor);
202        self.work
203            .lock()
204            .groups
205            .entry(group)
206            .or_default()
207            .buffer_allocations += 1;
208        TrackedBuffer {
209            buffer,
210            group,
211            graphics: self.clone(),
212        }
213    }
214
215    /// Creates a buffer containing its initial data.
216    pub fn create_buffer_init(
217        &self,
218        group: &'static str,
219        descriptor: &wgpu::util::BufferInitDescriptor<'_>,
220    ) -> TrackedBuffer {
221        let buffer = self.device.create_buffer_init(descriptor);
222        self.work
223            .lock()
224            .groups
225            .entry(group)
226            .or_default()
227            .buffer_allocations += 1;
228        TrackedBuffer {
229            buffer,
230            group,
231            graphics: self.clone(),
232        }
233    }
234
235    /// Uploads bytes to a buffer owned by this renderer.
236    pub fn write_buffer(&self, buffer: &TrackedBuffer, offset: u64, data: &[u8]) {
237        assert!(
238            Arc::ptr_eq(&self.work, &buffer.graphics.work),
239            "buffer belongs to another renderer"
240        );
241        self.queue.write_buffer(&buffer.buffer, offset, data);
242        let mut work = self.work.lock();
243        let group = work.groups.entry(buffer.group).or_default();
244        group.buffer_uploads += 1;
245        group.buffer_uploaded_bytes += data.len() as u64;
246    }
247
248    /// Creates a texture in a pipeline resource group.
249    pub fn create_texture(
250        &self,
251        group: &'static str,
252        descriptor: &wgpu::TextureDescriptor<'_>,
253    ) -> wgpu::Texture {
254        let texture = self.device.create_texture(descriptor);
255        self.work
256            .lock()
257            .groups
258            .entry(group)
259            .or_default()
260            .texture_allocations += 1;
261        texture
262    }
263
264    /// Uploads texture bytes through this renderer's queue.
265    pub fn write_texture(
266        &self,
267        group: &'static str,
268        destination: wgpu::TexelCopyTextureInfo<'_>,
269        data: &[u8],
270        layout: wgpu::TexelCopyBufferLayout,
271        size: wgpu::Extent3d,
272    ) {
273        match upload_band_rows(&destination, layout, size) {
274            Some((band_rows, block_height, bytes_per_row)) => {
275                let mut row = 0;
276                while row < size.height {
277                    let height = band_rows.min(size.height - row);
278                    let start = layout.offset + u64::from(row / block_height) * bytes_per_row;
279                    self.queue.write_texture(
280                        wgpu::TexelCopyTextureInfo {
281                            origin: wgpu::Origin3d {
282                                y: destination.origin.y + row,
283                                ..destination.origin
284                            },
285                            ..destination
286                        },
287                        &data[usize::try_from(start).expect("upload offset")..],
288                        wgpu::TexelCopyBufferLayout {
289                            offset: 0,
290                            bytes_per_row: layout.bytes_per_row,
291                            rows_per_image: None,
292                        },
293                        wgpu::Extent3d { height, ..size },
294                    );
295                    row += height;
296                }
297            }
298            None => self.queue.write_texture(destination, data, layout, size),
299        }
300        self.frame_uploads
301            .bytes
302            .fetch_add(data.len() as u64, Ordering::Relaxed);
303        let mut work = self.work.lock();
304        let group = work.groups.entry(group).or_default();
305        group.texture_uploads += 1;
306        group.texture_uploaded_bytes += data.len() as u64;
307    }
308}
309
310const FRAME_UPLOAD_BUDGET: u64 = 4 << 20;
311
312// Each queue upload stages its bytes in host-visible memory. Large single
313// uploads get dedicated upload heaps that drivers may keep resident after the
314// copy, so they are split into bands that fit the allocator's shared blocks.
315const UPLOAD_BAND_BYTES: u64 = 1 << 20;
316
317fn upload_band_rows(
318    destination: &wgpu::TexelCopyTextureInfo<'_>,
319    layout: wgpu::TexelCopyBufferLayout,
320    size: wgpu::Extent3d,
321) -> Option<(u32, u32, u64)> {
322    let bytes_per_row = u64::from(layout.bytes_per_row?);
323    let (_, block_height) = destination.texture.format().block_dimensions();
324    let block_rows = size.height.div_ceil(block_height);
325    if size.depth_or_array_layers != 1 || bytes_per_row * u64::from(block_rows) <= UPLOAD_BAND_BYTES
326    {
327        return None;
328    }
329    let band_blocks = u32::try_from((UPLOAD_BAND_BYTES / bytes_per_row).max(1)).ok()?;
330    Some((band_blocks * block_height, block_height, bytes_per_row))
331}
332
333impl Deref for GraphicsDevice {
334    type Target = wgpu::Device;
335    fn deref(&self) -> &Self::Target {
336        &self.device
337    }
338}
339
340/// A buffer whose resource group and renderer owner stay attached to uploads.
341#[derive(Clone, Debug)]
342pub struct TrackedBuffer {
343    buffer: wgpu::Buffer,
344    group: &'static str,
345    graphics: GraphicsDevice,
346}
347
348impl Deref for TrackedBuffer {
349    type Target = wgpu::Buffer;
350    fn deref(&self) -> &Self::Target {
351        &self.buffer
352    }
353}