1pub mod floor;
2pub mod pace;
3pub mod screens;
4pub mod stats;
5pub mod window;
6
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU8, Ordering};
9
10pub use floor::{
11 Capabilities, Floor, FormatNeed, GpuRefusal, GpuShortfall, StageBindings, backend_name,
12 layout_shortfall, live_floor, stage_bindings, trace_floor,
13};
14pub use wgpu;
15
16pub const HEADLESS_VAR: &str = "PFX_HEADLESS";
17
18pub fn headless_only() -> bool {
19 headless_set(std::env::var_os(HEADLESS_VAR).as_deref())
20}
21
22fn headless_set(value: Option<&std::ffi::OsStr>) -> bool {
23 value.is_some_and(|value| !value.is_empty() && value != "0")
24}
25
26pub fn refuse_window(what: &str) -> Result<(), String> {
27 if headless_only() {
28 Err(format!(
29 "{what}: refused, {HEADLESS_VAR} is set, so this is a headless render and no window or surface may open; render offscreen through Gpu::headless (pfx_game::Headless::with_gpu for a game)"
30 ))
31 } else {
32 Ok(())
33 }
34}
35
36const QUERY_PAIRS: u32 = 64;
37const QUERY_COUNT: u32 = QUERY_PAIRS * 2;
38const READ_SLOTS: usize = 3;
39const IDLE: u8 = 0;
40const MAPPING: u8 = 1;
41const READY: u8 = 2;
42const FAILED: u8 = 3;
43
44#[derive(Clone)]
45pub struct Gpu {
46 pub adapter: wgpu::Adapter,
47 pub device: wgpu::Device,
48 pub queue: wgpu::Queue,
49 pub info: wgpu::AdapterInfo,
50}
51
52pub struct OffscreenTarget {
53 pub texture: wgpu::Texture,
54 pub view: wgpu::TextureView,
55 pub format: wgpu::TextureFormat,
56 pub width: u32,
57 pub height: u32,
58}
59
60#[cfg(feature = "window")]
61pub struct WindowTarget {
62 pub surface: wgpu::Surface<'static>,
63 pub config: wgpu::SurfaceConfiguration,
64 pub present: window::PresentChoice,
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub struct PassTiming {
69 pub label: String,
70 pub milliseconds: f64,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq)]
74enum EntryKind {
75 Pass,
76 Mark,
77}
78
79#[derive(Clone, Debug, PartialEq)]
80struct Entry {
81 label: String,
82 kind: EntryKind,
83 index: u32,
84}
85
86struct Timeline {
87 entries: Vec<Entry>,
88 used: u32,
89 marks: bool,
90}
91
92impl Timeline {
93 fn new(marks: bool) -> Self {
94 Self {
95 entries: Vec::new(),
96 used: 0,
97 marks,
98 }
99 }
100
101 fn push(&mut self, label: String, kind: EntryKind, width: u32) -> Option<u32> {
102 if self.used + width > QUERY_COUNT {
103 return None;
104 }
105 let index = self.used;
106 self.used += width;
107 self.entries.push(Entry { label, kind, index });
108 Some(index)
109 }
110
111 fn pass(&mut self, label: String) -> Option<u32> {
112 self.push(label, EntryKind::Pass, 2)
113 }
114
115 fn mark(&mut self, label: String) -> Option<u32> {
116 if !self.marks {
117 return None;
118 }
119 self.push(label, EntryKind::Mark, 1)
120 }
121
122 fn take(&mut self) -> (Vec<Entry>, u32) {
123 let used = std::mem::take(&mut self.used);
124 (std::mem::take(&mut self.entries), used)
125 }
126}
127
128fn resolve_timings(entries: &[Entry], stamps: &[u64], period: f64) -> Vec<PassTiming> {
129 let milliseconds =
130 |first: u64, last: u64| last.saturating_sub(first) as f64 * period / 1_000_000.0;
131 let mut timings = Vec::with_capacity(entries.len());
132 let mut previous_mark: Option<u64> = None;
133 for entry in entries {
134 let index = entry.index as usize;
135 match entry.kind {
136 EntryKind::Pass => timings.push(PassTiming {
137 label: entry.label.clone(),
138 milliseconds: milliseconds(stamps[index], stamps[index + 1]),
139 }),
140 EntryKind::Mark => {
141 let stamp = stamps[index];
142 if let Some(earlier) = previous_mark {
143 timings.push(PassTiming {
144 label: entry.label.clone(),
145 milliseconds: milliseconds(earlier, stamp),
146 });
147 }
148 previous_mark = Some(stamp);
149 }
150 }
151 }
152 timings
153}
154
155struct ReadSlot {
156 buffer: wgpu::Buffer,
157 state: Arc<AtomicU8>,
158 entries: Vec<Entry>,
159 queries: u32,
160 busy: bool,
161 frame: u64,
162}
163
164struct Queries {
165 set: wgpu::QuerySet,
166 resolve: wgpu::Buffer,
167 slots: Vec<ReadSlot>,
168 timeline: Timeline,
169 period: f64,
170 dropped: u64,
171}
172
173pub struct GpuProfiler {
174 queries: Option<Queries>,
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub struct FrameTimings {
179 pub frame: u64,
180 pub passes: Vec<PassTiming>,
181}
182
183fn engine_backends() -> wgpu::Backends {
184 #[cfg(target_os = "windows")]
185 {
186 wgpu::Backends::VULKAN | wgpu::Backends::DX12
187 }
188 #[cfg(not(target_os = "windows"))]
189 {
190 wgpu::Backends::VULKAN | wgpu::Backends::METAL
191 }
192}
193
194fn instance() -> wgpu::Instance {
195 wgpu::Instance::new(wgpu::InstanceDescriptor {
196 backends: engine_backends(),
197 ..wgpu::InstanceDescriptor::new_without_display_handle()
198 })
199}
200
201fn engine_limits(supported: &wgpu::Limits) -> wgpu::Limits {
202 let floors = live_floor()
203 .limits
204 .or_better_values_from(&trace_floor().limits);
205 wgpu::Limits {
206 max_sampled_textures_per_shader_stage: supported
207 .max_sampled_textures_per_shader_stage
208 .min(floors.max_sampled_textures_per_shader_stage),
209 max_storage_buffer_binding_size: supported.max_storage_buffer_binding_size,
210 max_buffer_size: supported.max_buffer_size,
211 max_storage_buffers_per_shader_stage: supported
212 .max_storage_buffers_per_shader_stage
213 .min(floors.max_storage_buffers_per_shader_stage.max(16)),
214 max_storage_textures_per_shader_stage: supported
215 .max_storage_textures_per_shader_stage
216 .min(floors.max_storage_textures_per_shader_stage),
217 max_blas_primitive_count: supported.max_blas_primitive_count,
218 max_blas_geometry_count: supported.max_blas_geometry_count,
219 max_tlas_instance_count: supported.max_tlas_instance_count,
220 max_acceleration_structures_per_shader_stage: supported
221 .max_acceleration_structures_per_shader_stage,
222 ..wgpu::Limits::default()
223 }
224}
225
226fn default_limits(supported: &wgpu::Limits) -> wgpu::Limits {
227 wgpu::Limits {
228 max_texture_dimension_2d: supported.max_texture_dimension_2d,
229 max_texture_array_layers: supported.max_texture_array_layers,
230 ..engine_limits(supported)
231 }
232}
233
234fn requested_limits(
235 requested: &wgpu::Limits,
236 supported: &wgpu::Limits,
237) -> Result<wgpu::Limits, GpuShortfall> {
238 let mut refused = None;
239 requested.check_limits_with_fail_fn(supported, true, |name, needed, has| {
240 refused = Some(GpuShortfall::Limit { name, needed, has });
241 });
242 match refused {
243 Some(shortfall) => Err(shortfall),
244 None => Ok(requested
245 .clone()
246 .or_better_values_from(&engine_limits(supported))),
247 }
248}
249
250fn windows_backend_order() -> [wgpu::Backends; 2] {
251 [wgpu::Backends::VULKAN, wgpu::Backends::DX12]
252}
253
254fn preferred_adapter(
255 adapters: &[(wgpu::Backend, wgpu::DeviceType)],
256 windows: bool,
257) -> Option<usize> {
258 if windows {
259 adapters
260 .iter()
261 .enumerate()
262 .min_by_key(|(_, (backend, device))| {
263 (
264 match backend {
265 wgpu::Backend::Vulkan => 0u8,
266 wgpu::Backend::Dx12 => 1,
267 _ => 2,
268 },
269 u8::from(*device != wgpu::DeviceType::DiscreteGpu),
270 )
271 })
272 .map(|(index, _)| index)
273 } else {
274 adapters
275 .iter()
276 .position(|(_, device)| *device == wgpu::DeviceType::DiscreteGpu)
277 }
278}
279
280async fn choose_from(
281 instance: &wgpu::Instance,
282 surface: Option<&wgpu::Surface<'static>>,
283 backends: wgpu::Backends,
284 windows: bool,
285) -> Option<wgpu::Adapter> {
286 let mut adapters: Vec<_> = instance
287 .enumerate_adapters(backends)
288 .await
289 .into_iter()
290 .filter(|adapter| surface.is_none_or(|surface| adapter.is_surface_supported(surface)))
291 .collect();
292 let offers: Vec<_> = adapters
293 .iter()
294 .map(|adapter| {
295 let info = adapter.get_info();
296 (info.backend, info.device_type)
297 })
298 .collect();
299 preferred_adapter(&offers, windows).map(|index| adapters.swap_remove(index))
300}
301
302async fn pick_adapter(
303 instance: &wgpu::Instance,
304 surface: Option<&wgpu::Surface<'static>>,
305) -> Option<wgpu::Adapter> {
306 if cfg!(target_os = "windows") {
307 for backends in windows_backend_order() {
308 if let Some(adapter) = choose_from(instance, surface, backends, true).await {
309 return Some(adapter);
310 }
311 }
312 None
313 } else {
314 choose_from(instance, surface, engine_backends(), false).await
315 }
316}
317
318fn device_format_features(
319 adapter: &wgpu::Adapter,
320 device: &wgpu::Device,
321 format: wgpu::TextureFormat,
322) -> wgpu::TextureFormatFeatures {
323 let features = device.features();
324 if features.contains(wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES) {
325 adapter.get_texture_format_features(format)
326 } else {
327 format.guaranteed_format_features(features)
328 }
329}
330
331#[cfg(target_os = "linux")]
332const DRM_FORMAT_MODIFIER: &std::ffi::CStr = c"VK_EXT_image_drm_format_modifier";
333#[cfg(target_os = "linux")]
334const IMAGE_FORMAT_LIST: &std::ffi::CStr = c"VK_KHR_image_format_list";
335#[cfg(target_os = "linux")]
336const VULKAN_1_2: u32 = (1 << 22) | (2 << 12);
337
338#[cfg(target_os = "linux")]
339fn device_extensions(
340 supports: impl Fn(&std::ffi::CStr) -> bool,
341 api_version: u32,
342) -> Vec<&'static std::ffi::CStr> {
343 let format_list = supports(IMAGE_FORMAT_LIST);
344 if !supports(DRM_FORMAT_MODIFIER) || !(format_list || api_version >= VULKAN_1_2) {
345 return Vec::new();
346 }
347 if format_list {
348 vec![IMAGE_FORMAT_LIST, DRM_FORMAT_MODIFIER]
349 } else {
350 vec![DRM_FORMAT_MODIFIER]
351 }
352}
353
354async fn request_device(
355 adapter: &wgpu::Adapter,
356 desc: &wgpu::DeviceDescriptor<'_>,
357) -> Result<(wgpu::Device, wgpu::Queue), String> {
358 #[cfg(target_os = "linux")]
359 if let Some(opened) = open_vulkan_with_extensions(adapter, desc) {
360 return opened;
361 }
362 adapter
363 .request_device(desc)
364 .await
365 .map_err(|error| error.to_string())
366}
367
368#[cfg(target_os = "linux")]
369fn open_vulkan_with_extensions(
370 adapter: &wgpu::Adapter,
371 desc: &wgpu::DeviceDescriptor<'_>,
372) -> Option<Result<(wgpu::Device, wgpu::Queue), String>> {
373 let opened = {
374 let hal = unsafe { adapter.as_hal::<wgpu::hal::api::Vulkan>() }?;
375 let capabilities = hal.physical_device_capabilities();
376 let wanted = device_extensions(
377 |name| capabilities.supports_extension(name),
378 capabilities.properties().api_version,
379 );
380 if wanted.is_empty() {
381 return None;
382 }
383 unsafe {
384 hal.open_with_callback(
385 desc.required_features,
386 &desc.required_limits,
387 &desc.memory_hints,
388 Some(Box::new(move |args| {
389 for name in wanted {
390 if !args.extensions.contains(&name) {
391 args.extensions.push(name);
392 }
393 }
394 })),
395 )
396 }
397 };
398 Some(match opened {
399 Ok(opened) => unsafe { adapter.create_device_from_hal(opened, desc) }
400 .map_err(|error| error.to_string()),
401 Err(error) => Err(error.to_string()),
402 })
403}
404
405async fn open(
406 instance: &wgpu::Instance,
407 surface: Option<&wgpu::Surface<'static>>,
408 limits: Option<&wgpu::Limits>,
409 floor: Option<&Floor>,
410) -> Result<Gpu, GpuRefusal> {
411 let adapter = match pick_adapter(instance, surface).await {
412 Some(adapter) => adapter,
413 None => instance
414 .request_adapter(&wgpu::RequestAdapterOptions {
415 power_preference: wgpu::PowerPreference::HighPerformance,
416 compatible_surface: surface,
417 force_fallback_adapter: false,
418 apply_limit_buckets: false,
419 })
420 .await
421 .map_err(|_| GpuRefusal::new(GpuShortfall::NoAdapter, None))?,
422 };
423 let info = adapter.get_info();
424 let refuse = |shortfall| GpuRefusal::new(shortfall, Some(info.clone()));
425 let available = adapter.features();
426 let supported = adapter.limits();
427 let offered = Capabilities {
428 features: available,
429 downlevel: adapter.get_downlevel_capabilities().flags,
430 limits: supported.clone(),
431 };
432 if let Some(floor) = floor
433 && let Some(shortfall) = floor::shortfall(floor, &offered, |format| {
434 adapter.get_texture_format_features(format)
435 })
436 {
437 return Err(refuse(shortfall));
438 }
439 let required_features = available & floor::OPTIONAL_FEATURES;
440 let required_limits = match limits {
441 Some(requested) => requested_limits(requested, &supported).map_err(refuse)?,
442 None => default_limits(&supported),
443 };
444 let (device, queue) = request_device(
445 &adapter,
446 &wgpu::DeviceDescriptor {
447 label: Some("pfx"),
448 required_features,
449 required_limits,
450 experimental_features: if required_features
451 .contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY)
452 {
453 unsafe { wgpu::ExperimentalFeatures::enabled() }
454 } else {
455 wgpu::ExperimentalFeatures::disabled()
456 },
457 ..Default::default()
458 },
459 )
460 .await
461 .map_err(|error| refuse(GpuShortfall::Device(error)))?;
462 let gpu = Gpu {
463 adapter,
464 device,
465 queue,
466 info,
467 };
468 if let Some(floor) = floor {
469 gpu.check(floor)?;
470 }
471 Ok(gpu)
472}
473
474pub fn format_bytes_per_pixel(format: wgpu::TextureFormat) -> Option<u32> {
475 match format {
476 wgpu::TextureFormat::Rgba8UnormSrgb => Some(4),
477 wgpu::TextureFormat::Rgba16Float => Some(8),
478 wgpu::TextureFormat::Rgba32Float => Some(16),
479 _ => None,
480 }
481}
482
483pub fn padded_bytes_per_row(width: u32, format: wgpu::TextureFormat) -> Option<u32> {
484 let bytes = width.checked_mul(format_bytes_per_pixel(format)?)?;
485 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
486 bytes
487 .checked_add(alignment - 1)
488 .map(|n| n / alignment * alignment)
489}
490
491fn image_bytes(width: u32, height: u32, format: wgpu::TextureFormat) -> Result<usize, String> {
492 if width == 0 || height == 0 {
493 return Err("image dimensions must be nonzero".into());
494 }
495 let bpp = format_bytes_per_pixel(format).ok_or("unsupported target format")?;
496 usize::try_from(u64::from(width) * u64::from(height) * u64::from(bpp))
497 .map_err(|error| error.to_string())
498}
499
500impl Gpu {
501 pub async fn headless() -> Result<Self, String> {
502 Ok(open(&instance(), None, None, None).await?)
503 }
504
505 pub async fn headless_with(limits: wgpu::Limits) -> Result<Self, String> {
506 Ok(open(&instance(), None, Some(&limits), None).await?)
507 }
508
509 pub async fn headless_checked() -> Result<Self, GpuRefusal> {
510 open(&instance(), None, None, Some(&live_floor())).await
511 }
512
513 #[cfg(feature = "window")]
514 pub async fn window(
515 window: Arc<winit::window::Window>,
516 ) -> Result<(Self, WindowTarget), String> {
517 Ok(Self::open_window(window, None, None, &window::PresentPreference::fifo()).await?)
518 }
519
520 #[cfg(feature = "window")]
521 pub async fn window_with(
522 window: Arc<winit::window::Window>,
523 limits: wgpu::Limits,
524 ) -> Result<(Self, WindowTarget), String> {
525 Ok(Self::open_window(
526 window,
527 Some(&limits),
528 None,
529 &window::PresentPreference::fifo(),
530 )
531 .await?)
532 }
533
534 #[cfg(feature = "window")]
535 pub async fn window_checked(
536 window: Arc<winit::window::Window>,
537 ) -> Result<(Self, WindowTarget), GpuRefusal> {
538 Self::open_window(
539 window,
540 None,
541 Some(&live_floor()),
542 &window::PresentPreference::fifo(),
543 )
544 .await
545 }
546
547 #[cfg(feature = "window")]
548 pub async fn window_present(
549 window: Arc<winit::window::Window>,
550 present: window::PresentPreference,
551 ) -> Result<(Self, WindowTarget), String> {
552 Ok(Self::open_window(window, None, None, &present).await?)
553 }
554
555 #[cfg(feature = "window")]
556 async fn open_window(
557 window: Arc<winit::window::Window>,
558 limits: Option<&wgpu::Limits>,
559 floor: Option<&Floor>,
560 preference: &window::PresentPreference,
561 ) -> Result<(Self, WindowTarget), GpuRefusal> {
562 refuse_window("a window surface")
563 .map_err(|error| GpuRefusal::new(GpuShortfall::Surface(error), None))?;
564 let instance = instance();
565 let surface = instance
566 .create_surface(window.clone())
567 .map_err(|error| GpuRefusal::new(GpuShortfall::Surface(error.to_string()), None))?;
568 let gpu = open(&instance, Some(&surface), limits, floor).await?;
569 let refuse = |message: &str| {
570 GpuRefusal::new(
571 GpuShortfall::Surface(message.into()),
572 Some(gpu.info.clone()),
573 )
574 };
575 let caps = surface.get_capabilities(&gpu.adapter);
576 let format = caps
577 .formats
578 .iter()
579 .copied()
580 .find(wgpu::TextureFormat::is_srgb)
581 .or_else(|| caps.formats.first().copied())
582 .ok_or_else(|| refuse("surface has no supported formats"))?;
583 let alpha_mode = caps
584 .alpha_modes
585 .iter()
586 .copied()
587 .find(|mode| *mode == wgpu::CompositeAlphaMode::Opaque)
588 .or_else(|| caps.alpha_modes.first().copied())
589 .ok_or_else(|| refuse("surface has no supported alpha modes"))?;
590 let size = window.inner_size();
591 let present = window::choose_present(preference, &caps.present_modes);
592 let config = wgpu::SurfaceConfiguration {
593 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
594 format,
595 color_space: wgpu::SurfaceColorSpace::Auto,
596 width: size.width.max(1),
597 height: size.height.max(1),
598 present_mode: present.selected,
599 desired_maximum_frame_latency: present.desired_maximum_frame_latency,
600 alpha_mode,
601 view_formats: vec![],
602 };
603 surface.configure(&gpu.device, &config);
604 Ok((
605 gpu,
606 WindowTarget {
607 surface,
608 config,
609 present,
610 },
611 ))
612 }
613
614 pub fn check_floor(&self) -> Result<(), GpuRefusal> {
615 self.check(&live_floor())
616 }
617
618 pub fn check(&self, floor: &Floor) -> Result<(), GpuRefusal> {
619 let offered = Capabilities {
620 features: self.device.features(),
621 downlevel: self.adapter.get_downlevel_capabilities().flags,
622 limits: self.device.limits(),
623 };
624 match floor::shortfall(floor, &offered, |format| {
625 device_format_features(&self.adapter, &self.device, format)
626 }) {
627 Some(shortfall) => Err(GpuRefusal::new(shortfall, Some(self.info.clone()))),
628 None => Ok(()),
629 }
630 }
631
632 pub fn profiling(&self) -> bool {
633 floor::timestamps(self.device.features())
634 }
635
636 pub fn offscreen(
637 &self,
638 width: u32,
639 height: u32,
640 format: wgpu::TextureFormat,
641 ) -> Result<OffscreenTarget, String> {
642 image_bytes(width, height, format)?;
643 let supported = self.adapter.get_texture_format_features(format);
644 let usage = wgpu::TextureUsages::RENDER_ATTACHMENT
645 | wgpu::TextureUsages::TEXTURE_BINDING
646 | wgpu::TextureUsages::COPY_SRC
647 | wgpu::TextureUsages::COPY_DST;
648 if !supported.allowed_usages.contains(usage) {
649 return Err(format!(
650 "adapter cannot use {format:?} as an offscreen target"
651 ));
652 }
653 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
654 label: Some("pfx offscreen"),
655 size: wgpu::Extent3d {
656 width,
657 height,
658 depth_or_array_layers: 1,
659 },
660 mip_level_count: 1,
661 sample_count: 1,
662 dimension: wgpu::TextureDimension::D2,
663 format,
664 usage,
665 view_formats: &[],
666 });
667 let view = texture.create_view(&Default::default());
668 Ok(OffscreenTarget {
669 texture,
670 view,
671 format,
672 width,
673 height,
674 })
675 }
676
677 pub fn upload_rgba8(&self, target: &OffscreenTarget, pixels: &[u8]) -> Result<(), String> {
678 if target.format != wgpu::TextureFormat::Rgba8UnormSrgb {
679 return Err("RGBA8 upload requires an RGBA8 target".into());
680 }
681 self.upload_bytes(target, pixels)
682 }
683
684 pub fn upload_rgba16(&self, target: &OffscreenTarget, pixels: &[u16]) -> Result<(), String> {
685 if target.format != wgpu::TextureFormat::Rgba16Float {
686 return Err("RGBA16 upload requires an RGBA16 target".into());
687 }
688 if pixels.len() * 2 != image_bytes(target.width, target.height, target.format)? {
689 return Err("RGBA16 image length does not match target".into());
690 }
691 let bytes: Vec<u8> = pixels
692 .iter()
693 .flat_map(|value| value.to_le_bytes())
694 .collect();
695 self.upload_bytes(target, &bytes)
696 }
697
698 fn upload_bytes(&self, target: &OffscreenTarget, pixels: &[u8]) -> Result<(), String> {
699 if pixels.len() != image_bytes(target.width, target.height, target.format)? {
700 return Err("image length does not match target".into());
701 }
702 let row = target.width * format_bytes_per_pixel(target.format).unwrap();
703 self.queue.write_texture(
704 target.texture.as_image_copy(),
705 pixels,
706 wgpu::TexelCopyBufferLayout {
707 offset: 0,
708 bytes_per_row: Some(row),
709 rows_per_image: Some(target.height),
710 },
711 target.texture.size(),
712 );
713 Ok(())
714 }
715
716 pub fn readback_bytes(&self, target: &OffscreenTarget) -> Result<Vec<u8>, String> {
717 let row = padded_bytes_per_row(target.width, target.format).ok_or("row size overflow")?;
718 let raw_row = target.width * format_bytes_per_pixel(target.format).unwrap();
719 let size = u64::from(row) * u64::from(target.height);
720 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
721 label: Some("pfx readback"),
722 size,
723 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
724 mapped_at_creation: false,
725 });
726 let mut encoder = self
727 .device
728 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
729 label: Some("pfx readback"),
730 });
731 encoder.copy_texture_to_buffer(
732 target.texture.as_image_copy(),
733 wgpu::TexelCopyBufferInfo {
734 buffer: &buffer,
735 layout: wgpu::TexelCopyBufferLayout {
736 offset: 0,
737 bytes_per_row: Some(row),
738 rows_per_image: Some(target.height),
739 },
740 },
741 target.texture.size(),
742 );
743 self.queue.submit(Some(encoder.finish()));
744 let (send, receive) = std::sync::mpsc::channel();
745 let slice = buffer.slice(..);
746 slice.map_async(wgpu::MapMode::Read, move |result| {
747 let _ = send.send(result);
748 });
749 self.device
750 .poll(wgpu::PollType::wait_indefinitely())
751 .map_err(|error| format!("{error:?}"))?;
752 receive
753 .recv()
754 .map_err(|error| error.to_string())?
755 .map_err(|error| error.to_string())?;
756 let mapped = slice
757 .get_mapped_range()
758 .map_err(|error| error.to_string())?;
759 let mut bytes =
760 Vec::with_capacity(image_bytes(target.width, target.height, target.format)?);
761 for padded in mapped.chunks_exact(row as usize) {
762 bytes.extend_from_slice(&padded[..raw_row as usize]);
763 }
764 drop(mapped);
765 buffer.unmap();
766 Ok(bytes)
767 }
768
769 pub fn readback_rgba8(&self, target: &OffscreenTarget) -> Result<Vec<u8>, String> {
770 if target.format != wgpu::TextureFormat::Rgba8UnormSrgb {
771 return Err("RGBA8 readback requires an RGBA8 target".into());
772 }
773 self.readback_bytes(target)
774 }
775
776 pub fn readback_rgba16(&self, target: &OffscreenTarget) -> Result<Vec<u16>, String> {
777 if target.format != wgpu::TextureFormat::Rgba16Float {
778 return Err("RGBA16 readback requires an RGBA16 target".into());
779 }
780 Ok(self
781 .readback_bytes(target)?
782 .chunks_exact(2)
783 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
784 .collect())
785 }
786}
787
788#[cfg(feature = "window")]
789impl WindowTarget {
790 pub fn resize(&mut self, gpu: &Gpu, width: u32, height: u32) {
791 if width == 0 || height == 0 {
792 return;
793 }
794 self.config.width = width;
795 self.config.height = height;
796 self.surface.configure(&gpu.device, &self.config);
797 }
798
799 pub fn set_present(
800 &mut self,
801 gpu: &Gpu,
802 preference: &window::PresentPreference,
803 ) -> window::PresentChoice {
804 let caps = self.surface.get_capabilities(&gpu.adapter);
805 let choice = window::choose_present(preference, &caps.present_modes);
806 self.config.present_mode = choice.selected;
807 self.config.desired_maximum_frame_latency = choice.desired_maximum_frame_latency;
808 self.present = choice.clone();
809 self.surface.configure(&gpu.device, &self.config);
810 choice
811 }
812}
813
814impl GpuProfiler {
815 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
816 if !floor::timestamps(device.features()) {
817 return Self { queries: None };
818 }
819 let size = u64::from(QUERY_COUNT) * 8;
820 let set = device.create_query_set(&wgpu::QuerySetDescriptor {
821 label: Some("pfx timestamps"),
822 ty: wgpu::QueryType::Timestamp,
823 count: QUERY_COUNT,
824 });
825 let resolve = device.create_buffer(&wgpu::BufferDescriptor {
826 label: Some("pfx timestamp resolve"),
827 size,
828 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
829 mapped_at_creation: false,
830 });
831 let slots = (0..READ_SLOTS)
832 .map(|_| ReadSlot {
833 buffer: device.create_buffer(&wgpu::BufferDescriptor {
834 label: Some("pfx timestamp read"),
835 size,
836 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
837 mapped_at_creation: false,
838 }),
839 state: Arc::new(AtomicU8::new(IDLE)),
840 entries: Vec::new(),
841 queries: 0,
842 busy: false,
843 frame: 0,
844 })
845 .collect();
846 Self {
847 queries: Some(Queries {
848 set,
849 resolve,
850 slots,
851 timeline: Timeline::new(
852 device
853 .features()
854 .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS),
855 ),
856 period: f64::from(queue.get_timestamp_period()),
857 dropped: 0,
858 }),
859 }
860 }
861
862 pub fn read_slots(&self) -> usize {
863 self.queries
864 .as_ref()
865 .map_or(0, |queries| queries.slots.len())
866 }
867
868 pub fn in_flight(&self) -> usize {
869 self.queries.as_ref().map_or(0, |queries| {
870 queries.slots.iter().filter(|slot| slot.busy).count()
871 })
872 }
873
874 pub fn dropped(&self) -> u64 {
875 self.queries.as_ref().map_or(0, |queries| queries.dropped)
876 }
877
878 pub fn pass(&mut self, label: impl Into<String>) -> Option<u32> {
879 self.queries.as_mut()?.timeline.pass(label.into())
880 }
881
882 pub fn marks_supported(&self) -> bool {
883 self.queries
884 .as_ref()
885 .is_some_and(|queries| queries.timeline.marks)
886 }
887
888 pub fn mark(&mut self, encoder: &mut wgpu::CommandEncoder, label: impl Into<String>) {
889 let Some(queries) = self.queries.as_mut() else {
890 return;
891 };
892 if let Some(index) = queries.timeline.mark(label.into()) {
893 encoder.write_timestamp(&queries.set, index);
894 }
895 }
896
897 pub fn compute_writes(
898 &self,
899 index: Option<u32>,
900 ) -> Option<wgpu::ComputePassTimestampWrites<'_>> {
901 let queries = self.queries.as_ref()?;
902 index.map(|index| wgpu::ComputePassTimestampWrites {
903 query_set: &queries.set,
904 beginning_of_pass_write_index: Some(index),
905 end_of_pass_write_index: Some(index + 1),
906 })
907 }
908
909 pub fn render_writes(&self, index: Option<u32>) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
910 let queries = self.queries.as_ref()?;
911 index.map(|index| wgpu::RenderPassTimestampWrites {
912 query_set: &queries.set,
913 beginning_of_pass_write_index: Some(index),
914 end_of_pass_write_index: Some(index + 1),
915 })
916 }
917
918 pub fn finish(&mut self, encoder: &mut wgpu::CommandEncoder) -> Option<usize> {
919 self.finish_frame(encoder, 0)
920 }
921
922 pub fn finish_frame(
923 &mut self,
924 encoder: &mut wgpu::CommandEncoder,
925 frame: u64,
926 ) -> Option<usize> {
927 let queries = self.queries.as_mut()?;
928 if queries.timeline.entries.is_empty() {
929 return None;
930 }
931 let slot = queries.slots.iter().position(|slot| !slot.busy);
932 let (entries, count) = queries.timeline.take();
933 let Some(slot_index) = slot else {
934 queries.dropped += 1;
935 return None;
936 };
937 encoder.resolve_query_set(&queries.set, 0..count, &queries.resolve, 0);
938 encoder.copy_buffer_to_buffer(
939 &queries.resolve,
940 0,
941 &queries.slots[slot_index].buffer,
942 0,
943 u64::from(count) * 8,
944 );
945 queries.slots[slot_index].entries = entries;
946 queries.slots[slot_index].queries = count;
947 queries.slots[slot_index].busy = true;
948 queries.slots[slot_index].frame = frame;
949 Some(slot_index)
950 }
951
952 pub fn submitted(&mut self, slot: usize) {
953 let Some(read) = self
954 .queries
955 .as_mut()
956 .and_then(|queries| queries.slots.get_mut(slot))
957 else {
958 return;
959 };
960 if !read.busy || read.state.load(Ordering::Acquire) != IDLE {
961 return;
962 }
963 read.state.store(MAPPING, Ordering::Release);
964 let state = read.state.clone();
965 read.buffer.slice(..u64::from(read.queries) * 8).map_async(
966 wgpu::MapMode::Read,
967 move |result| {
968 state.store(
969 if result.is_ok() { READY } else { FAILED },
970 Ordering::Release,
971 );
972 },
973 );
974 }
975
976 pub fn collect(&mut self, device: &wgpu::Device) -> Vec<Vec<PassTiming>> {
977 self.collect_frames(device)
978 .into_iter()
979 .map(|frame| frame.passes)
980 .collect()
981 }
982
983 pub fn collect_frames(&mut self, device: &wgpu::Device) -> Vec<FrameTimings> {
984 let _ = device.poll(wgpu::PollType::Poll);
985 self.gather()
986 }
987
988 pub fn gather(&mut self) -> Vec<FrameTimings> {
989 let Some(queries) = self.queries.as_mut() else {
990 return Vec::new();
991 };
992 let mut frames = Vec::new();
993 for slot in &mut queries.slots {
994 match slot.state.load(Ordering::Acquire) {
995 READY => {
996 let timings = {
997 let data = slot
998 .buffer
999 .slice(..u64::from(slot.queries) * 8)
1000 .get_mapped_range()
1001 .expect("a mapped timing slot");
1002 let stamps: Vec<u64> = data
1003 .chunks_exact(8)
1004 .map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap()))
1005 .collect();
1006 resolve_timings(&slot.entries, &stamps, queries.period)
1007 };
1008 slot.buffer.unmap();
1009 slot.busy = false;
1010 slot.entries.clear();
1011 slot.state.store(IDLE, Ordering::Release);
1012 frames.push(FrameTimings {
1013 frame: slot.frame,
1014 passes: timings,
1015 });
1016 }
1017 FAILED => {
1018 slot.busy = false;
1019 slot.entries.clear();
1020 slot.state.store(IDLE, Ordering::Release);
1021 queries.dropped += 1;
1022 }
1023 _ => {}
1024 }
1025 }
1026 frames.sort_by_key(|frame| frame.frame);
1027 frames
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1036 fn a_headless_render_is_any_value_but_empty_or_zero() {
1037 use std::ffi::OsStr;
1038 assert!(headless_set(Some(OsStr::new("1"))));
1039 assert!(headless_set(Some(OsStr::new("yes"))));
1040 assert!(!headless_set(Some(OsStr::new("0"))));
1041 assert!(!headless_set(Some(OsStr::new(""))));
1042 assert!(!headless_set(None));
1043 }
1044
1045 #[test]
1046 fn windows_takes_vulkan_before_dx12_and_other_targets_keep_the_first_discrete() {
1047 let discrete = wgpu::DeviceType::DiscreteGpu;
1048 let integrated = wgpu::DeviceType::IntegratedGpu;
1049 let dx12_then_vulkan = [
1050 (wgpu::Backend::Dx12, discrete),
1051 (wgpu::Backend::Vulkan, discrete),
1052 ];
1053 assert_eq!(
1054 windows_backend_order(),
1055 [wgpu::Backends::VULKAN, wgpu::Backends::DX12]
1056 );
1057 assert_eq!(preferred_adapter(&dx12_then_vulkan, true), Some(1));
1058 assert_eq!(preferred_adapter(&dx12_then_vulkan, false), Some(0));
1059 assert_eq!(
1060 preferred_adapter(
1061 &[
1062 (wgpu::Backend::Dx12, discrete),
1063 (wgpu::Backend::Vulkan, integrated),
1064 ],
1065 true,
1066 ),
1067 Some(1)
1068 );
1069 assert_eq!(
1070 preferred_adapter(
1071 &[
1072 (wgpu::Backend::Dx12, integrated),
1073 (wgpu::Backend::Dx12, discrete),
1074 ],
1075 true,
1076 ),
1077 Some(1)
1078 );
1079 assert_eq!(
1080 preferred_adapter(
1081 &[
1082 (wgpu::Backend::Vulkan, discrete),
1083 (wgpu::Backend::Vulkan, discrete),
1084 ],
1085 true,
1086 ),
1087 Some(0)
1088 );
1089 assert_eq!(preferred_adapter(&[], true), None);
1090 assert_eq!(
1091 preferred_adapter(&[(wgpu::Backend::Vulkan, integrated)], false),
1092 None
1093 );
1094 }
1095
1096 #[test]
1097 fn row_sizes_and_formats() {
1098 assert_eq!(
1099 format_bytes_per_pixel(wgpu::TextureFormat::Rgba8UnormSrgb),
1100 Some(4)
1101 );
1102 assert_eq!(
1103 format_bytes_per_pixel(wgpu::TextureFormat::Rgba16Float),
1104 Some(8)
1105 );
1106 assert_eq!(
1107 format_bytes_per_pixel(wgpu::TextureFormat::Rgba32Float),
1108 Some(16)
1109 );
1110 assert_eq!(format_bytes_per_pixel(wgpu::TextureFormat::R8Unorm), None);
1111 assert_eq!(
1112 padded_bytes_per_row(1, wgpu::TextureFormat::Rgba8UnormSrgb),
1113 Some(256)
1114 );
1115 assert_eq!(
1116 padded_bytes_per_row(64, wgpu::TextureFormat::Rgba8UnormSrgb),
1117 Some(256)
1118 );
1119 assert_eq!(
1120 padded_bytes_per_row(65, wgpu::TextureFormat::Rgba8UnormSrgb),
1121 Some(512)
1122 );
1123 assert_eq!(
1124 padded_bytes_per_row(33, wgpu::TextureFormat::Rgba16Float),
1125 Some(512)
1126 );
1127 assert_eq!(
1128 padded_bytes_per_row(17, wgpu::TextureFormat::Rgba32Float),
1129 Some(512)
1130 );
1131 assert_eq!(
1132 padded_bytes_per_row(u32::MAX, wgpu::TextureFormat::Rgba32Float),
1133 None
1134 );
1135 }
1136
1137 #[test]
1138 fn marks_become_intervals_named_after_the_later_mark() {
1139 let mut timeline = Timeline::new(true);
1140 assert_eq!(timeline.mark("a".into()), Some(0));
1141 assert_eq!(timeline.mark("b".into()), Some(1));
1142 assert_eq!(timeline.mark("c".into()), Some(2));
1143 let (entries, used) = timeline.take();
1144 assert_eq!(used, 3);
1145 let timings = resolve_timings(&entries, &[1_000_000, 4_000_000, 10_000_000], 1.0);
1146 assert_eq!(
1147 timings,
1148 vec![
1149 PassTiming {
1150 label: "b".into(),
1151 milliseconds: 3.0
1152 },
1153 PassTiming {
1154 label: "c".into(),
1155 milliseconds: 6.0
1156 },
1157 ]
1158 );
1159 }
1160
1161 #[test]
1162 fn a_single_mark_gives_no_timing() {
1163 let mut timeline = Timeline::new(true);
1164 timeline.mark("a".into());
1165 let (entries, _) = timeline.take();
1166 assert!(resolve_timings(&entries, &[5], 1.0).is_empty());
1167 }
1168
1169 #[test]
1170 fn marks_and_passes_mix_in_call_order() {
1171 let mut timeline = Timeline::new(true);
1172 assert_eq!(timeline.mark("start".into()), Some(0));
1173 assert_eq!(timeline.pass("vis".into()), Some(1));
1174 assert_eq!(timeline.mark("trace".into()), Some(3));
1175 assert_eq!(timeline.pass("post".into()), Some(4));
1176 assert_eq!(timeline.mark("blit".into()), Some(6));
1177 let (entries, used) = timeline.take();
1178 assert_eq!(used, 7);
1179 let stamps = [0, 100, 300, 1_000, 2_000, 2_500, 4_000];
1180 let timings = resolve_timings(&entries, &stamps, 1_000_000.0);
1181 let summary: Vec<(&str, f64)> = timings
1182 .iter()
1183 .map(|timing| (timing.label.as_str(), timing.milliseconds))
1184 .collect();
1185 assert_eq!(
1186 summary,
1187 vec![
1188 ("vis", 200.0),
1189 ("trace", 1_000.0),
1190 ("post", 500.0),
1191 ("blit", 3_000.0),
1192 ]
1193 );
1194 }
1195
1196 #[test]
1197 fn passes_resolve_like_before() {
1198 let mut timeline = Timeline::new(false);
1199 assert_eq!(timeline.pass("a".into()), Some(0));
1200 assert_eq!(timeline.pass("b".into()), Some(2));
1201 let (entries, used) = timeline.take();
1202 assert_eq!(used, 4);
1203 let timings = resolve_timings(&entries, &[10, 30, 30, 90], 2.0);
1204 assert_eq!(timings[0].milliseconds, 40.0 / 1_000_000.0);
1205 assert_eq!(timings[1].milliseconds, 120.0 / 1_000_000.0);
1206 }
1207
1208 #[test]
1209 fn marks_without_the_feature_record_nothing() {
1210 let mut timeline = Timeline::new(false);
1211 assert_eq!(timeline.mark("a".into()), None);
1212 assert_eq!(timeline.mark("b".into()), None);
1213 assert_eq!(timeline.pass("p".into()), Some(0));
1214 let (entries, used) = timeline.take();
1215 assert_eq!(used, 2);
1216 assert_eq!(entries.len(), 1);
1217 }
1218
1219 #[test]
1220 fn capacity_counts_queries_and_resets_after_take() {
1221 let mut timeline = Timeline::new(true);
1222 for _ in 0..QUERY_PAIRS {
1223 assert!(timeline.pass("p".into()).is_some());
1224 }
1225 assert_eq!(timeline.pass("p".into()), None);
1226 assert_eq!(timeline.mark("m".into()), None);
1227 timeline.take();
1228 assert_eq!(timeline.mark("m".into()), Some(0));
1229 for _ in 0..QUERY_COUNT - 1 {
1230 assert!(timeline.mark("m".into()).is_some());
1231 }
1232 assert_eq!(timeline.mark("m".into()), None);
1233 assert_eq!(timeline.pass("p".into()), None);
1234 }
1235
1236 #[test]
1237 #[ignore = "needs a GPU; run under pgpu"]
1238 fn device_and_readback() {
1239 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1240 let rgba8 = gpu
1241 .offscreen(65, 3, wgpu::TextureFormat::Rgba8UnormSrgb)
1242 .unwrap();
1243 let bytes = vec![137; 65 * 3 * 4];
1244 gpu.upload_rgba8(&rgba8, &bytes).unwrap();
1245 assert_eq!(gpu.readback_rgba8(&rgba8).unwrap(), bytes);
1246 let rgba16 = gpu
1247 .offscreen(33, 3, wgpu::TextureFormat::Rgba16Float)
1248 .unwrap();
1249 let halves = vec![0x3c00; 33 * 3 * 4];
1250 gpu.upload_rgba16(&rgba16, &halves).unwrap();
1251 assert_eq!(gpu.readback_rgba16(&rgba16).unwrap(), halves);
1252 if gpu
1253 .offscreen(17, 2, wgpu::TextureFormat::Rgba32Float)
1254 .is_ok()
1255 {
1256 assert_eq!(
1257 padded_bytes_per_row(17, wgpu::TextureFormat::Rgba32Float),
1258 Some(512)
1259 );
1260 }
1261 }
1262
1263 #[test]
1264 fn default_limits_take_the_adapter_texture_sizes() {
1265 let supported = wgpu::Limits {
1266 max_texture_dimension_2d: 16_384,
1267 max_texture_array_layers: 2_048,
1268 max_buffer_size: 1 << 33,
1269 ..wgpu::Limits::default()
1270 };
1271 let limits = default_limits(&supported);
1272 assert_eq!(limits.max_texture_dimension_2d, 16_384);
1273 assert_eq!(limits.max_texture_array_layers, 2_048);
1274 assert_eq!(limits.max_buffer_size, 1 << 33);
1275 assert_eq!(
1276 limits.max_bind_groups,
1277 wgpu::Limits::default().max_bind_groups
1278 );
1279 }
1280
1281 #[test]
1282 fn the_device_gets_the_floor_wherever_the_adapter_offers_it() {
1283 let supported = wgpu::Limits {
1284 max_texture_dimension_2d: 32_768,
1285 max_sampled_textures_per_shader_stage: 1_000_000,
1286 max_storage_buffers_per_shader_stage: 1_000_000,
1287 ..wgpu::Limits::default()
1288 };
1289 let floor = live_floor();
1290 assert!(floor.limits.check_limits(&default_limits(&supported)));
1291 let requested = wgpu::Limits {
1292 max_texture_dimension_2d: 16_384,
1293 ..wgpu::Limits::default()
1294 };
1295 assert!(
1296 floor
1297 .limits
1298 .check_limits(&requested_limits(&requested, &supported).unwrap())
1299 );
1300 }
1301
1302 #[test]
1303 fn the_device_gets_the_trace_floor_wherever_the_adapter_offers_it() {
1304 let supported = wgpu::Limits {
1305 max_storage_buffers_per_shader_stage: 1_000_000,
1306 max_storage_buffer_binding_size: u64::from(u32::MAX),
1307 max_buffer_size: 1 << 40,
1308 ..wgpu::Limits::default()
1309 };
1310 let floor = trace_floor();
1311 assert!(floor.limits.check_limits(&default_limits(&supported)));
1312 assert!(
1313 floor
1314 .limits
1315 .check_limits(&requested_limits(&wgpu::Limits::default(), &supported).unwrap())
1316 );
1317 let at = floor.limits.clone();
1318 assert!(floor.limits.check_limits(&default_limits(&at)));
1319 }
1320
1321 #[cfg(target_os = "linux")]
1322 #[test]
1323 #[ignore = "needs a GPU; run under pgpu"]
1324 fn a_vulkan_device_enables_drm_format_modifiers_when_offered() {
1325 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1326 let offered =
1327 unsafe { gpu.adapter.as_hal::<wgpu::hal::api::Vulkan>() }.is_some_and(|hal| {
1328 hal.physical_device_capabilities()
1329 .supports_extension(DRM_FORMAT_MODIFIER)
1330 });
1331 if !offered {
1332 return;
1333 }
1334 let device = unsafe { gpu.device.as_hal::<wgpu::hal::api::Vulkan>() }.unwrap();
1335 let enabled = device.enabled_device_extensions();
1336 assert!(enabled.contains(&DRM_FORMAT_MODIFIER));
1337 assert!(enabled.contains(&c"VK_EXT_external_memory_dma_buf"));
1338 assert!(enabled.contains(&c"VK_KHR_external_memory_fd"));
1339 }
1340
1341 #[test]
1342 #[ignore = "needs a GPU; run under pgpu"]
1343 fn this_machine_meets_the_live_floor() {
1344 let gpu = pollster::block_on(Gpu::headless_checked()).unwrap();
1345 gpu.check_floor().unwrap();
1346 let plain = pollster::block_on(Gpu::headless()).unwrap();
1347 plain.check_floor().unwrap();
1348 gpu.check(&trace_floor()).unwrap();
1349 plain.check(&trace_floor()).unwrap();
1350 println!(
1351 "{} ({} {}), profiling {}",
1352 gpu.info.name,
1353 gpu.info.driver,
1354 gpu.info.driver_info,
1355 gpu.profiling()
1356 );
1357 }
1358
1359 #[test]
1360 fn a_request_beyond_the_adapter_names_the_limit() {
1361 let supported = wgpu::Limits::default();
1362 let request = wgpu::Limits {
1363 max_texture_dimension_2d: supported.max_texture_dimension_2d + 1,
1364 ..wgpu::Limits::default()
1365 };
1366 let error = requested_limits(&request, &supported).unwrap_err();
1367 assert!(
1368 matches!(
1369 error,
1370 GpuShortfall::Limit {
1371 name: "max_texture_dimension_2d",
1372 ..
1373 }
1374 ),
1375 "{error}"
1376 );
1377 }
1378
1379 #[test]
1380 fn a_request_within_the_adapter_keeps_the_engine_baseline() {
1381 let supported = wgpu::Limits {
1382 max_texture_dimension_2d: 16_384,
1383 max_buffer_size: 1 << 33,
1384 ..wgpu::Limits::default()
1385 };
1386 let request = wgpu::Limits {
1387 max_texture_dimension_2d: 12_000,
1388 ..wgpu::Limits::default()
1389 };
1390 let limits = requested_limits(&request, &supported).unwrap();
1391 assert_eq!(limits.max_texture_dimension_2d, 12_000);
1392 assert_eq!(limits.max_buffer_size, 1 << 33);
1393 }
1394
1395 #[test]
1396 #[ignore = "needs a GPU; run under pgpu"]
1397 fn the_default_reports_the_adapter_texture_maximum() {
1398 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1399 let supported = gpu.adapter.limits();
1400 let limits = gpu.device.limits();
1401 assert_eq!(
1402 limits.max_texture_dimension_2d,
1403 supported.max_texture_dimension_2d
1404 );
1405 assert_eq!(
1406 limits.max_texture_array_layers,
1407 supported.max_texture_array_layers
1408 );
1409 }
1410
1411 #[test]
1412 #[ignore = "needs a GPU; run under pgpu"]
1413 fn headless_with_honours_a_requested_limit() {
1414 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1415 let supported = gpu.adapter.limits();
1416 let wanted = supported.max_texture_dimension_2d.min(10_000);
1417 drop(gpu);
1418 let gpu = pollster::block_on(Gpu::headless_with(wgpu::Limits {
1419 max_texture_dimension_2d: wanted,
1420 ..wgpu::Limits::default()
1421 }))
1422 .unwrap();
1423 assert_eq!(gpu.device.limits().max_texture_dimension_2d, wanted);
1424 }
1425
1426 #[test]
1427 #[ignore = "needs a GPU; run under pgpu"]
1428 fn headless_with_refuses_a_limit_beyond_the_adapter() {
1429 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1430 let beyond = gpu.adapter.limits().max_texture_dimension_2d + 1;
1431 drop(gpu);
1432 let error = pollster::block_on(Gpu::headless_with(wgpu::Limits {
1433 max_texture_dimension_2d: beyond,
1434 ..wgpu::Limits::default()
1435 }))
1436 .err()
1437 .unwrap();
1438 assert!(error.contains("max_texture_dimension_2d"), "{error}");
1439 }
1440
1441 #[test]
1442 #[ignore = "needs a GPU; run under pgpu"]
1443 fn timing() {
1444 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1445 let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
1446 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1447 let index = profiler.pass("clear");
1448 let target = gpu
1449 .offscreen(8, 8, wgpu::TextureFormat::Rgba8UnormSrgb)
1450 .unwrap();
1451 {
1452 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1453 multiview_mask: None,
1454 label: Some("clear"),
1455 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1456 view: &target.view,
1457 resolve_target: None,
1458 ops: wgpu::Operations {
1459 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1460 store: wgpu::StoreOp::Store,
1461 },
1462 depth_slice: None,
1463 })],
1464 depth_stencil_attachment: None,
1465 timestamp_writes: profiler.render_writes(index),
1466 occlusion_query_set: None,
1467 });
1468 }
1469 let slot = profiler.finish(&mut encoder);
1470 gpu.queue.submit(Some(encoder.finish()));
1471 if let Some(slot) = slot {
1472 profiler.submitted(slot);
1473 gpu.device
1474 .poll(wgpu::PollType::wait_indefinitely())
1475 .unwrap();
1476 let frames = profiler.collect(&gpu.device);
1477 assert_eq!(frames.len(), 1);
1478 assert_eq!(frames[0][0].label, "clear");
1479 assert!(frames[0][0].milliseconds.is_finite());
1480 } else {
1481 assert!(index.is_none());
1482 }
1483 }
1484
1485 #[test]
1486 #[ignore = "needs a GPU; run under pgpu"]
1487 fn a_full_ring_drops_the_frame_and_late_frames_come_back_tagged() {
1488 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1489 let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
1490 if profiler.read_slots() == 0 {
1491 return;
1492 }
1493 let target = gpu
1494 .offscreen(8, 8, wgpu::TextureFormat::Rgba8UnormSrgb)
1495 .unwrap();
1496 let mut slots = Vec::new();
1497 for frame in 0..=READ_SLOTS as u64 {
1498 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1499 let index = profiler.pass("clear");
1500 drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1501 multiview_mask: None,
1502 label: Some("clear"),
1503 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1504 view: &target.view,
1505 resolve_target: None,
1506 ops: wgpu::Operations {
1507 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1508 store: wgpu::StoreOp::Store,
1509 },
1510 depth_slice: None,
1511 })],
1512 depth_stencil_attachment: None,
1513 timestamp_writes: profiler.render_writes(index),
1514 occlusion_query_set: None,
1515 }));
1516 let slot = profiler.finish_frame(&mut encoder, 40 + frame);
1517 gpu.queue.submit(Some(encoder.finish()));
1518 slots.push(slot);
1519 }
1520 assert!(slots[..READ_SLOTS].iter().all(Option::is_some));
1521 assert!(slots[READ_SLOTS].is_none());
1522 assert_eq!(profiler.dropped(), 1);
1523 assert_eq!(profiler.in_flight(), READ_SLOTS);
1524 for slot in slots.into_iter().flatten() {
1525 profiler.submitted(slot);
1526 }
1527 gpu.device
1528 .poll(wgpu::PollType::wait_indefinitely())
1529 .unwrap();
1530 let frames = profiler.collect_frames(&gpu.device);
1531 assert_eq!(
1532 frames.iter().map(|frame| frame.frame).collect::<Vec<_>>(),
1533 (40..40 + READ_SLOTS as u64).collect::<Vec<_>>()
1534 );
1535 assert_eq!(profiler.in_flight(), 0);
1536 }
1537
1538 #[test]
1539 #[ignore = "needs a GPU; run under pgpu"]
1540 fn marks_around_two_dispatches() {
1541 let gpu = pollster::block_on(Gpu::headless()).unwrap();
1542 let mut profiler = GpuProfiler::new(&gpu.device, &gpu.queue);
1543 if !profiler.marks_supported() {
1544 return;
1545 }
1546 let module = gpu
1547 .device
1548 .create_shader_module(wgpu::ShaderModuleDescriptor {
1549 label: Some("marks"),
1550 source: wgpu::ShaderSource::Wgsl("@compute @workgroup_size(1) fn main() {}".into()),
1551 });
1552 let pipeline = gpu
1553 .device
1554 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1555 label: Some("marks"),
1556 layout: None,
1557 module: &module,
1558 entry_point: Some("main"),
1559 compilation_options: Default::default(),
1560 cache: None,
1561 });
1562 let mut encoder = gpu.device.create_command_encoder(&Default::default());
1563 profiler.mark(&mut encoder, "start");
1564 for _ in 0..2 {
1565 {
1566 let mut pass = encoder.begin_compute_pass(&Default::default());
1567 pass.set_pipeline(&pipeline);
1568 pass.dispatch_workgroups(1, 1, 1);
1569 }
1570 profiler.mark(&mut encoder, "dispatch");
1571 }
1572 let slot = profiler.finish(&mut encoder).unwrap();
1573 gpu.queue.submit(Some(encoder.finish()));
1574 profiler.submitted(slot);
1575 gpu.device
1576 .poll(wgpu::PollType::wait_indefinitely())
1577 .unwrap();
1578 let frames = profiler.collect(&gpu.device);
1579 assert_eq!(frames.len(), 1);
1580 assert_eq!(frames[0].len(), 2);
1581 for timing in &frames[0] {
1582 assert_eq!(timing.label, "dispatch");
1583 assert!(timing.milliseconds.is_finite());
1584 assert!(timing.milliseconds >= 0.0);
1585 }
1586 }
1587}