concinnity-device 0.19.119

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
//! The device layer a Vulkan context builds on: the platform window, instance,
//! debug messenger, surface, device, swapchain and allocator a fresh launch
//! acquires, or adopts from the outgoing context on a live world reload.

use ash::vk;
use concinnity_core::components;
use concinnity_core::render::backend_init::{self, PostSettings};
use concinnity_core::render::error::RenderResult;
use concinnity_core::render::hdr_output;
use concinnity_core::render::pass_timing;
use std::ffi::{CStr, CString, c_char};

use crate::vulkan::context::{SwapchainState, VkHardware};
use crate::vulkan::device::*;
use crate::vulkan::swapchain::*;

// The backend inputs the hardware is acquired or adopted with.
pub(super) struct HardwareRequest<'a> {
    pub(super) window: &'a components::Window,
    pub(super) validation: bool,
    pub(super) frames: usize,
    pub(super) vsync: bool,
    pub(super) post: &'a PostSettings,
}

impl HardwareRequest<'_> {
    // The swap-decision key for a future live reload of the context (see
    // `hot_swap_config`). Normalized `frames` (>=1) matches how
    // `BackendInit::swapchain_config` clamps it.
    fn swapchain_config(&self) -> backend_init::SwapchainConfig {
        backend_init::SwapchainConfig {
            frames_in_flight: self.frames,
            hdr_display: self.post.hdr_display,
            hdr_pq: self.post.hdr_pq,
        }
    }
}

// Adopt the hardware an outgoing context handed over on a live editor reload.
// The swapchain is inherited, but its image views are this context's own (the
// outgoing context frees its views), and the vsync and swap settings are the
// incoming world's.
pub(super) fn inherit_hardware(
    (mut hw, mut swapchain): (VkHardware, SwapchainState),
    req: &HardwareRequest<'_>,
) -> RenderResult<(VkHardware, SwapchainState)> {
    swapchain.image_views =
        create_swapchain_image_views(&hw.device, &swapchain.images, swapchain.format)?;
    hw.vsync = req.vsync;
    hw.swapchain_config = req.swapchain_config();
    Ok((hw, swapchain))
}

// Acquire the hardware for a fresh launch. `upscale_sdk` stays alive through
// device creation, since its instance-extension pointers and XeSS feature chain
// are read there.
pub(super) fn acquire_hardware(
    req: &HardwareRequest<'_>,
) -> RenderResult<(VkHardware, SwapchainState)> {
    let (title, width, height, title_bar) = (
        req.window.title.as_str(),
        req.window.width,
        req.window.height,
        req.window.title_bar,
    );
    let HardwareRequest {
        validation,
        frames,
        vsync,
        post,
        ..
    } = *req;
    let hdr_display = post.hdr_display;
    let (temporal_upscaling, upscale_backend) = (post.temporal_upscaling, post.upscale_backend);
    // Platform window: native Win32 on Windows, AppKit on macOS, GLFW on Linux.
    let mut window = crate::vulkan::window::PlatformWindow::new(
        title,
        width,
        height,
        &components::WindowMode::Windowed,
        true,
        title_bar,
    )?;

    let entry = crate::vulkan::loader::load_entry()?;

    // Resolve which (if any) upscaler SDK needs Vulkan instance / device
    // extensions enabled at creation time (DLSS / XeSS). Queried before
    // instance creation (it needs at most the loaded SDK), then threaded
    // into `create_logical_device` for the device extensions / features.
    // Inert (`choice == Native`) when upscaling is off or the backend needs
    // nothing; held in scope until after device creation so its
    // instance-ext pointers + XeSS feature chain stay valid. Resolved before
    // `app_info` so its `min_api_version` can raise the instance apiVersion.
    let upscale_sdk = crate::vulkan::post::UpscaleSdk::prepare(temporal_upscaling, upscale_backend);

    let app_name = CString::new(title).unwrap_or_default();
    let engine_name = CString::new("Concinnity").unwrap();
    // Vulkan 1.2 baseline: FidelityFX FSR's precompiled shaders are SPIR-V
    // 1.5, valid only under a 1.2+ instance. XeSS 3.x raises the floor to
    // 1.3 (its shaders use SPV_KHR_integer_dot_product, a 1.3 capability),
    // reported via `min_api_version`. Take the max, clamped to what the
    // loader actually supports so an unsupported request can't fail instance
    // creation (the backend then falls back). The engine's own shaders are
    // unaffected by the bump.
    // SAFETY: an enumeration query on a live instance handle; it only reads, and ash
    // sizes the output vector from the count the driver reports.
    let loader_version = unsafe { entry.try_enumerate_instance_version() }
        .ok()
        .flatten()
        .unwrap_or(vk::API_VERSION_1_2);
    let api_version = vk::API_VERSION_1_2
        .max(upscale_sdk.min_api_version())
        .min(loader_version);
    let app_info = vk::ApplicationInfo::default()
        .application_name(&app_name)
        .application_version(vk::make_api_version(0, 0, 1, 0))
        .engine_name(&engine_name)
        .engine_version(vk::make_api_version(0, 0, 1, 0))
        .api_version(api_version);

    // Hold the windowing extension name CStrings in scope so their pointers
    // stay valid through instance creation, then drop with the rest of init
    // (mirrors `device.rs`'s `enabled`/`ext_names` pairing). The later
    // pushes are all `'static` NAME pointers, so they need no backing store.
    let instance_ext_cstrings: Vec<CString> = window
        .required_instance_extensions()
        .iter()
        .map(|s| CString::new(s.as_str()).unwrap())
        .collect();
    let mut ext_names_raw: Vec<*const c_char> =
        instance_ext_cstrings.iter().map(|c| c.as_ptr()).collect();

    let debug_ext = ash::ext::debug_utils::NAME.as_ptr();
    if validation {
        ext_names_raw.push(debug_ext);
    }

    // The optional instance extensions the loader actually exposes:
    // `VK_EXT_swapchain_colorspace` for the extended-range surface
    // formats HDR output needs, and `VK_KHR_portability_enumeration`
    // so a portability driver (MoltenVK) is enumerable at all. A
    // missing one degrades rather than failing instance creation.
    let available_ext_props =
        // SAFETY: an enumeration query on a live instance handle; it only reads, and
        // ash sizes the output vector from the count the driver reports.
        unsafe { entry.enumerate_instance_extension_properties(None) }
            .unwrap_or_default();
    let optional_exts = crate::vulkan::instance_exts::select(
        &crate::vulkan::instance_exts::names_of(&available_ext_props),
        hdr_display,
    );
    let swapchain_colorspace_ext_available = optional_exts.swapchain_colorspace;
    if hdr_display && !swapchain_colorspace_ext_available {
        tracing::warn!(
            "HDR display requested but VK_EXT_swapchain_colorspace is not exposed by the \
     Vulkan loader; falling back to SDR (BGRA8 sRGB) output"
        );
    }
    ext_names_raw.extend(optional_exts.names().iter().map(|n| n.as_ptr()));

    // Instance extensions the chosen upscaler SDK requires (DLSS / XeSS).
    // The pointers borrow from `upscale_sdk`, which outlives this scope.
    for ptr in upscale_sdk.instance_extension_ptrs() {
        ext_names_raw.push(ptr);
    }

    let layer_names_raw: Vec<*const c_char> = if validation {
        // Leaked: the instance borrows the name for its whole lifetime.
        let layer = CString::new("VK_LAYER_KHRONOS_validation").unwrap();
        vec![layer.into_raw().cast_const()]
    } else {
        vec![]
    };

    let instance_info = vk::InstanceCreateInfo::default()
        .application_info(&app_info)
        .flags(optional_exts.flags())
        .enabled_extension_names(&ext_names_raw)
        .enabled_layer_names(&layer_names_raw);

    // SAFETY: the create-info and every slice it borrows are live for the call, and
    // each handle it names belongs to this device.
    let instance = unsafe { entry.create_instance(&instance_info, None) }
        .map_err(|e| crate::vulkan::error::map_vk_result(e, "create instance"))?;
    // A run with no layer messages looks exactly like a run the layer
    // found nothing wrong with, so say which one happened. Reaching
    // here with the layer requested means it loaded: a missing
    // `VK_LAYER_KHRONOS_validation` fails instance creation above.
    if validation {
        tracing::info!("vulkan validation layer: enabled");
    }

    // Budget the messenger callback consumes to drop benign DLSS first-frame
    // layout errors; set after `build_upscaler` resolves to DLSS. Heap-boxed
    // so its address stays stable, and handed to the owning device
    // handle alongside the messenger: the callback reads it for as long
    // as the messenger can fire, which is past the device teardown.
    // `None` when validation (the messenger) is off.
    let debug_filter: Option<Box<std::sync::atomic::AtomicU32>> =
        validation.then(|| Box::new(std::sync::atomic::AtomicU32::new(0)));
    let (debug_utils, debug_messenger) = if validation {
        let du = ash::ext::debug_utils::Instance::new(&entry, &instance);
        let user_data = debug_filter
            .as_ref()
            .map(|b| &**b as *const std::sync::atomic::AtomicU32 as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());
        let info = vk::DebugUtilsMessengerCreateInfoEXT::default()
            .message_severity(
                vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
                    | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
            )
            .message_type(
                vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
                    | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
                    | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
            )
            .pfn_user_callback(Some(debug_callback))
            .user_data(user_data);
        // SAFETY: the create-info and every slice it borrows are live for the call, and
        // each handle it names belongs to this device.
        let messenger = unsafe { du.create_debug_utils_messenger(&info, None) }
            .map_err(|e| crate::vulkan::error::map_vk_result(e, "debug messenger"))?;
        (Some(du), Some(messenger))
    } else {
        (None, None)
    };

    let surface_loader = ash::khr::surface::Instance::new(&entry, &instance);
    let surface = window.create_surface(&entry, &instance)?;

    let (physical_device, graphics_family, present_family) =
        pick_physical_device(&instance, &surface_loader, surface)?;

    // Logical device. `rt_capable` comes back true when the device exposes
    // the ray-query extension set (and XeSS is not the active backend); the
    // RT extensions are enabled whenever capable so a live RT toggle works,
    // independent of whether the world wants RT at launch. The
    // acceleration-structure build + RT pass below are gated on
    // `rt_settings.is_some() && rt_capable` (everything falls back to SSR
    // when RT is off or the device is incapable).
    let crate::vulkan::device::LogicalDevice {
        device,
        memory_budget: memory_budget_supported,
        rt_capable,
        caps,
        update_after_bind,
    } = create_logical_device(
        &instance,
        physical_device,
        graphics_family,
        present_family,
        &upscale_sdk,
    )?;
    // Hand the raw device to the owning wrapper straight away: from
    // here on the device, the instance and the entry are destroyed
    // by the last handle to them, and every Vulkan object the
    // backend owns retires through this device's queue.
    let device = crate::vulkan::owned::VkDevice::new(
        entry.clone(),
        instance.clone(),
        device,
        frames,
        crate::vulkan::owned::DebugMessenger {
            utils: debug_utils,
            messenger: debug_messenger,
            filter: debug_filter,
        },
        caps,
    );

    // SAFETY: a property query on a live handle; it only reads.
    let graphics_queue = unsafe { device.get_device_queue(graphics_family, 0) };
    // SAFETY: a property query on a live handle; it only reads.
    let present_queue = unsafe { device.get_device_queue(present_family, 0) };

    // Timestamp support: the per-frame GPU-time chip uses a query pool
    // with `2 * frames` slots, a pair per in-flight frame. `timestamp_period`
    // is nanoseconds-per-tick; `timestamp_valid_bits` on the graphics queue
    // family must be non-zero for `cmd_write_timestamp` to be valid. Without
    // either the renderer leaves `gpu_frame_us` at zero. Mirrors
    // `directx::build_timestamp_resources`.
    let device_props =
        // SAFETY: a property query on a live handle; it only reads.
        unsafe { instance.get_physical_device_properties(physical_device) };

    // Persisted pipeline cache: seeded from disk when a blob for
    // this device exists, handed to every pipeline creation below.
    crate::vulkan::pipeline_cache::install(&device, &device_props);

    // SAFETY: a property query on a live handle; it only reads.
    let queue_family_props =
        unsafe { instance.get_physical_device_queue_family_properties(physical_device) };
    let timestamp_period = device_props.limits.timestamp_period;
    let timestamp_valid_bits = queue_family_props
        .get(graphics_family as usize)
        .map(|f| f.timestamp_valid_bits)
        .unwrap_or(0);
    let timestamps_supported = timestamp_period > 0.0 && timestamp_valid_bits > 0;
    let timestamp_query_pool = if timestamps_supported {
        // One per-frame block of `SLOTS_PER_FRAME` slots (whole-frame pair +
        // one pair per render pass) per frame in flight.
        //
        // Timing: the start buffer resets the whole block and writes the
        // whole-frame start; each per-pass command buffer writes its own pair
        // around its encode; the end buffer writes the whole-frame end. Unlike
        // D3D12, which can pre-write every slot so a pass that did not run still
        // reads a value, Vulkan forbids writing a timestamp to a query already
        // written without an intervening reset. A pass absent from this frame's
        // graph therefore leaves its reset-but-unwritten slots unavailable; the
        // readback uses WITH_AVAILABILITY and reports 0 for any pair that is not
        // both available.
        let info = vk::QueryPoolCreateInfo::default()
            .query_type(vk::QueryType::TIMESTAMP)
            .query_count((pass_timing::SLOTS_PER_FRAME * frames) as u32);
        // SAFETY: the create-info and every slice it borrows are live for the call, and
        // each handle it names belongs to this device.
        match unsafe { device.create_query_pool(&info, None) } {
            Ok(p) => Some(p),
            Err(e) => {
                tracing::warn!("timestamp query pool create failed: {e}");
                None
            }
        }
    } else {
        None
    };

    // Device-local heap indices for the VRAM-residency chip. Sums
    // `heap_usage` on every DEVICE_LOCAL heap when `VK_EXT_memory_budget`
    // is supported; otherwise the field stays empty and the chip reports
    // zero (matching DirectX's adapter-without-QueryVideoMemoryInfo
    // fallback).
    let memory_props =
        // SAFETY: a property query on a live handle; it only reads.
        unsafe { instance.get_physical_device_memory_properties(physical_device) };
    let device_local_heaps: Vec<u32> = if memory_budget_supported {
        (0..memory_props.memory_heap_count as usize)
            .filter(|i| {
                memory_props.memory_heaps[*i]
                    .flags
                    .contains(vk::MemoryHeapFlags::DEVICE_LOCAL)
            })
            .map(|i| i as u32)
            .collect()
    } else {
        Vec::new()
    };

    let hdr_mode = resolve_hdr_mode(
        &surface_loader,
        physical_device,
        surface,
        swapchain_colorspace_ext_available,
        post,
    );
    let swapchain_loader = ash::khr::swapchain::Device::new(&instance, &device);
    let (swapchain, swapchain_images, swapchain_format, swapchain_extent) = create_swapchain_inner(
        &SwapchainSurface {
            instance: &instance,
            device: &device,
            pd: physical_device,
            surface_loader: &surface_loader,
            surface,
            swapchain_loader: &swapchain_loader,
        },
        SwapchainQueueFamilies {
            graphics_family,
            present_family,
        },
        SwapchainConfig {
            width,
            height,
            old_swapchain: vk::SwapchainKHR::null(),
            hdr_mode,
            vsync,
        },
    )?;
    let swapchain_image_views =
        create_swapchain_image_views(&device, &swapchain_images, swapchain_format)?;

    // The device allocator every pooled buffer / image is placed
    // through, built before any resource creation so init-time
    // resources can pool. A reload inherits the outgoing
    // context's instead (the other match arm), so the rebuilt
    // world places into the blocks the old world releases.
    let alloc =
        crate::vulkan::allocator::DeviceAllocator::new(&instance, physical_device, &device, frames);

    Ok((
        VkHardware {
            instance,
            device,
            physical_device,
            surface,
            surface_loader,
            graphics_queue,
            present_queue,
            graphics_family,
            alloc,
            timestamp_query_pool,
            timestamp_period_ns: timestamp_period,
            device_local_heaps,
            memory_budget_supported,
            rt_capable,
            update_after_bind,
            hdr_mode,
            vsync,
            swapchain_config: req.swapchain_config(),
            window: Some(window),
            _entry: entry,
        },
        SwapchainState {
            loader: swapchain_loader,
            handle: swapchain,
            images: swapchain_images,
            image_views: swapchain_image_views,
            format: swapchain_format,
            extent: swapchain_extent,
            last_present_index: None,
        },
    ))
}

// Resolve the swapchain output mode from the world's HDR request and the color
// space pairs the surface advertises.
fn resolve_hdr_mode(
    surface_loader: &ash::khr::surface::Instance,
    physical_device: vk::PhysicalDevice,
    surface: vk::SurfaceKHR,
    swapchain_colorspace_ext_available: bool,
    post: &PostSettings,
) -> hdr_output::HdrOutputMode {
    let (hdr_display, hdr_pq) = (post.hdr_display, post.hdr_pq);
    // HDR-output resolve. The world's `hdr_display` toggle is the
    // gate; even on a capable display, no HDR unless the asset opts
    // in. The reverse (`hdr_display = true` on an SDR-only surface,
    // or with the color-space loader extension missing) falls back
    // to SDR with a logged warning. Vulkan has no portable max-EDR
    // query: when the surface advertises the scRGB-linear color
    // space we synthesize a placeholder `max_edr = 2.0` (the
    // HDR400-class minimum) so the shared `HdrOutputMode::resolve`
    // logic stays uniform across backends.
    // Probe which HDR color-space pairs the surface advertises. An
    // advertised HDR color space is Vulkan's "HDR available" signal (there
    // is no portable max-EDR query), so we synthesize the placeholder
    // `max_edr` from it. scRGB-linear drives the extended-linear path; an
    // `HDR10_ST2084_EXT` pair (float or 10-bit packed) drives the PQ path.
    // SAFETY: a property query on a live handle; it only reads.
    let surface_formats =
        unsafe { surface_loader.get_physical_device_surface_formats(physical_device, surface) }
            .unwrap_or_default();
    let advertises = |fmt: vk::Format, cs: vk::ColorSpaceKHR| {
        surface_formats
            .iter()
            .any(|f| f.format == fmt && f.color_space == cs)
    };
    let scrgb_advertises = swapchain_colorspace_ext_available
        && advertises(
            vk::Format::R16G16B16A16_SFLOAT,
            vk::ColorSpaceKHR::EXTENDED_SRGB_LINEAR_EXT,
        );
    let pq_advertises = swapchain_colorspace_ext_available
        && (advertises(
            vk::Format::R16G16B16A16_SFLOAT,
            vk::ColorSpaceKHR::HDR10_ST2084_EXT,
        ) || advertises(
            vk::Format::A2B10G10R10_UNORM_PACK32,
            vk::ColorSpaceKHR::HDR10_ST2084_EXT,
        ));
    // PQ needs the HDR10 color space. When `hdr_pq` is requested but only
    // scRGB is advertised, fall back to the extended-linear path so the
    // shader encode and the swapchain color space never diverge (sending
    // PQ-encoded values to an scRGB-linear swapchain would look wrong).
    let pq_capable = hdr_pq && pq_advertises;
    if hdr_display && hdr_pq && !pq_advertises {
        tracing::warn!(
            "HDR display + hdr_pq:true requested but no surface format advertises HDR10 PQ \
     (RGBA16F / A2B10G10R10_UNORM_PACK32 + HDR10_ST2084_EXT); falling back to \
     scRGB-linear extended-range output"
        );
    }
    let max_edr = if scrgb_advertises || pq_advertises {
        2.0
    } else {
        1.0
    };
    let hdr_mode = hdr_output::HdrOutputMode::resolve(hdr_display, pq_capable, max_edr);
    if hdr_display && !hdr_mode.is_hdr() {
        tracing::warn!(
            "HDR display requested but no surface format advertises an HDR color space \
     (scRGB linear or HDR10 PQ): falling back to SDR (BGRA8 sRGB) output"
        );
    } else if hdr_mode.pq_flag() > 0.5 {
        tracing::info!("HDR display output enabled: HDR10 PQ swapchain (SMPTE ST 2084)");
    } else if hdr_mode.is_hdr() {
        tracing::info!(
            "HDR display output enabled: scRGB-linear swapchain (RGBA16F + \
     EXTENDED_SRGB_LINEAR_EXT)"
        );
    }
    hdr_mode
}

// Validation layer debug callback: logs validation errors and warnings.
// DLSS's first EvaluateFeature samples two NGX-internal resources it leaves in
// UNDEFINED, tripping VUID-vkCmdDraw-None-09600 exactly twice per feature
// creation. They are internal to nvngx_dlss.dll (not bindable through the NGX
// parameter API, confirmed by supplying our own exposure input, which did not
// displace them) and benign (the upscale output is correct). The debug messenger
// drops this many such messages while DLSS is the active upscaler. D3D12 never
// surfaces them (it has no image-layout validation model).
pub(in crate::vulkan) const DLSS_FIRST_FRAME_LAYOUT_SUPPRESS: u32 = 2;

// Decide whether to drop a validation message rather than log it: true only for
// the benign DLSS first-frame layout VUID while `budget` is positive (consuming
// one unit of it). Every other VUID, and an exhausted budget, returns false so
// the message still surfaces. Split out from `debug_callback` so the suppression
// logic is unit testable without a live Vulkan instance.
fn drop_benign_dlss_layout_error(message_id: &[u8], budget: &std::sync::atomic::AtomicU32) -> bool {
    if message_id != b"VUID-vkCmdDraw-None-09600" {
        return false;
    }
    budget
        .fetch_update(
            std::sync::atomic::Ordering::Relaxed,
            std::sync::atomic::Ordering::Relaxed,
            |n| (n > 0).then(|| n - 1),
        )
        .is_ok()
}

// Validation messages route through here (installed only when validation is on).
// `user` is a `*const AtomicU32`: a budget of benign DLSS first-frame layout
// errors to drop, set after `build_upscaler` resolves to DLSS (and reset on
// resize, which re-creates the feature). Null when no budget is wired.
unsafe extern "system" fn debug_callback(
    severity: vk::DebugUtilsMessageSeverityFlagsEXT,
    _msg_type: vk::DebugUtilsMessageTypeFlagsEXT,
    data: *const vk::DebugUtilsMessengerCallbackDataEXT,
    user: *mut std::ffi::c_void,
) -> vk::Bool32 {
    if data.is_null() {
        return vk::FALSE;
    }
    // SAFETY: the null check above passed, and Vulkan guarantees the callback data outlives the
    // callback.
    let data = unsafe { &*data };

    // Drop the benign DLSS first-frame layout errors (see the helper); any other
    // VUID, or an exhausted budget, still logs.
    if !user.is_null() && !data.p_message_id_name.is_null() {
        // SAFETY: Vulkan fills `extension_name` with a NUL-terminated string, and the borrow does
        // not outlive the properties entry it points into.
        let vuid = unsafe { CStr::from_ptr(data.p_message_id_name) };
        // SAFETY: `user` is the `AtomicU32` budget pointer this messenger was registered with; it
        // is non-null per the check above and outlives the messenger.
        let budget = unsafe { &*(user as *const std::sync::atomic::AtomicU32) };
        if drop_benign_dlss_layout_error(vuid.to_bytes(), budget) {
            return vk::FALSE;
        }
    }

    // SAFETY: Vulkan fills `p_message` with a NUL-terminated string that lives for the duration of
    // the callback.
    let msg = unsafe { CStr::from_ptr(data.p_message) }.to_string_lossy();
    if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
        tracing::error!("[Vulkan] {}", msg);
    } else {
        tracing::warn!("[Vulkan] {}", msg);
    }
    vk::FALSE
}

#[cfg(test)]
mod tests {
    use super::{DLSS_FIRST_FRAME_LAYOUT_SUPPRESS, drop_benign_dlss_layout_error};
    use std::sync::atomic::{AtomicU32, Ordering};

    const LAYOUT_VUID: &[u8] = b"VUID-vkCmdDraw-None-09600";

    #[test]
    fn drops_exactly_the_budgeted_layout_errors_then_logs() {
        let budget = AtomicU32::new(DLSS_FIRST_FRAME_LAYOUT_SUPPRESS);
        for _ in 0..DLSS_FIRST_FRAME_LAYOUT_SUPPRESS {
            assert!(drop_benign_dlss_layout_error(LAYOUT_VUID, &budget));
        }
        // Budget spent: a further occurrence logs, so a real bug would surface.
        assert!(!drop_benign_dlss_layout_error(LAYOUT_VUID, &budget));
        assert_eq!(budget.load(Ordering::Relaxed), 0);
    }

    #[test]
    fn never_drops_other_vuids_or_touches_budget() {
        let budget = AtomicU32::new(DLSS_FIRST_FRAME_LAYOUT_SUPPRESS);
        assert!(!drop_benign_dlss_layout_error(
            b"VUID-vkCmdDraw-None-02699",
            &budget
        ));
        assert!(!drop_benign_dlss_layout_error(b"", &budget));
        assert_eq!(
            budget.load(Ordering::Relaxed),
            DLSS_FIRST_FRAME_LAYOUT_SUPPRESS
        );
    }

    #[test]
    fn drops_nothing_when_budget_is_zero() {
        let budget = AtomicU32::new(0);
        assert!(!drop_benign_dlss_layout_error(LAYOUT_VUID, &budget));
    }
}