Skip to main content

pfx_gpu/
lib.rs

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}