18 KiB
18 KiB
renderer_device
Purpose
renderer_device is the Vulkan instance, logical-device, queue, synchronization, and device-owned-object layer extracted from renderer. It owns Vulkan device lifecycle and shared device services; higher-level resource and rendering abstractions belong in renderer.
Module map
lib.rs: crate error/result types, physical-device feature/property descriptions and queries, surface capabilities, extension helpers, and public module exports.extern crate self as renderer_devicesupports generated derive paths.device.rs:Device/DeviceInner, logical-device creation, pools, ownership traits/wrappers, extension and allocation helpers.instance.rs:Instance/InstanceInner, instance setup, adapter discovery/selection, surface capability queries, debug messenger lifecycle.queue.rs: queue-family selection,Queue, queue synchronization, andDeviceQueues.sync.rs: fence/semaphore wrappers and pools,FenceFuture, and background fence waiters.pipeline_cache.rs: keyed Vulkan pipeline cache withpipeline_cache.binload/export.debug.rs: Vulkan debug callback forwarding messages to tracing.renderer_macros/src/lib.rs: separate workspace proc-macro source; derivesDeviceReffor a struct field marked#[device].
Key types and ownership
Device(Arc<DeviceInner>)is the cloneable owning handle. It dereferences toDeviceInner; equality isArc::ptr_eq.DeviceInnerkeeps the rawash::Device, allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inlineDevicePools.Device::enabled_features()returns the feature snapshot requested for logical-device creation; it is distinct fromDevice::features(), which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.PhysicalDeviceInfo::create_logical_deviceselects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructsDeviceInnerwithArc::new_cyclic.DeviceInner::dev,instance,queues,physical_device,main_queue,compute_queue, andtransfer_queueare the main convenience accessors for downstream code.DevicePoolscontains aPipelineCachewrapper plus pools for raw fences and binary/timeline semaphore newtypes. APool<T>stores bare handles inMutex<Vec<T>>and aWeak<DeviceInner>;get()reuses a handle or creates one throughPooled.PoolObject<T: Recycle>strongly owns aDevice; dropping it returns its handle to the relevant pool. The pool's weak reference prevents aDeviceInnerownership cycle.asdf::DeviceObject<T>strongly owns itsDevice, dereferences to its handle, and destroys that handle throughasdf::traits::ExternallyManagedObject<Device>on drop.DeviceOwned<T>exposes the owning device and raw handle.DeviceRefsuppliesdevice()forDevice, Arc/reference forms, and structs supported by theDeviceRefderive (#[device]identifies the field).Pooledcreates pooled handles;Recyclereturns them.- The
DeviceRefproc macro is implemented in the separaterenderer_macrospackage and emits paths rooted at::renderer_device::. sync::Fenceandsync::SemaphoredistinguishDedicated(DeviceObject) fromPooled(PoolObject) ownership.SemaphoreInner,BinarySemaphore, andTimelineSemaphorerepresent semaphore-kind handles.PhysicalDeviceInfo,PhysicalDeviceFeatures,PhysicalDeviceProperties, andSurfaceCapabilitiescarry adapter/capability data.Instance::choose_adapter_defaultacceptsOption<vk::SurfaceKHR>(a raw handle), not a swapchain/surface wrapper.ErrorandResult<T>are the crate-wide error types;Errorcovers Vulkan, loading, allocator, I/O, and renderer-facing resource/capability failures.InstanceDescconfigures borrowed instance extensions/layers, optional app metadata, and a raw display handle; its default requests surface and validation layers, with validation settings in debug builds.Instance::newcreates a sharedInstanceand requests debug-utils plus optional capability-query extensions alongside requested/platform extensions.Instance::expose_adapterqueries one adapter.choose_adapter_defaultranks enumerated adapters against requested extensions/features and prefers discrete devices, then newer APIs, device-local memory, and texture size.PhysicalDeviceFeaturesstores core, Vulkan 1.1/1.2/1.3, and optional mesh-shader feature structures;push_to_device_create_infochains enabled feature structs.PhysicalDevicePropertiesstores supported extensions, core/versioned properties, optional mesh-shader properties, memory properties, and API version;supports_extensionchecks name and minimum version.get_physical_device_featuresandget_physical_device_propertiesquery the feature/property chains. Extension query results are consumed by the instance and logical-device setup helpers.DeviceExtensionsholds the debug-utils device loader and optional swapchain/mesh-shader loaders.DeviceInner.enabled_extensionsrecords enabled extension names.AllocationStrategyselects allocator-managed blocks or a dedicated allocation.AllocationrepresentsOwned,Shared, orUnmanagedGPU allocations.get_extensionsreturns supported/enabled and unsupported requested extension sets;get_layersreturns available requested layers or the supported/unsupported split.Queueshares aQueueInnerthroughArc;with_lockedserializes operations on the Vulkan queue.QueueFamilyrecords index, count, flags, granularity, and timestamp bits.DeviceQueuesprovides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires the selected graphics family to support presentation.DeviceQueueInfoschooses queue families and producesvk::DeviceQueueCreateInfovalues beforeretrieve_queuesobtains the raw Vulkan queues.QueueFlagsindevice.rsdescribes requested graphics/async-compute/transfer/present families;queue::QueueFlagsdescribes discovered Vulkan family capabilities. They are distinct types.FenceFuturewaits and resets a fence, andSyncThreadpoolschedules blocking fence waits for async wakers.Fence::from_poolandSemaphore::from_poolwrap recycled pool entries.SemaphoreTypeselects binary or timeline construction.SemaphoreInnerwraps the common Vulkan handle whileBinarySemaphoreandTimelineSemaphoretag their respective pools.Fence::wait_on,is_signaled, andresetoperate on either dedicated or pooled fence handles through their owning device.- Pooled timeline semaphores are created at value zero;
Semaphore::from_poolsignals the requested initial value when it obtains one. PipelineCache::newcomputes its key from adapter vendor/API/device/driver properties and tries to load compatible data; cache persistence uses a magic and format version.DebugNameis the wrapper for Vulkan object debug names.Extensionis the name/version pair used by extension helpers and adapter creation.make_extension!,get_extensions,get_layers, and the support/intersection functions centralize Vulkan extension discovery.SurfaceCapabilitiesreturns rawSurfaceCapabilitiesKHRplus the effective minimum image count, surface formats, and present modes queried for the raw surface.DebugUtilsCreateInfois the small severity/message-type configuration record used with debug utilities;DebugUtilsitself owns the instance loader and messenger handle.InstanceExtensionsstores optional surface-capability-query extension loaders and an optional surface-maintenance marker.DeviceHandleis a separate unsafe mutable-handle destruction trait used byvk::DescriptorPool; the normalDeviceObjectdestruction path usesExternallyManagedObjectinstead.asdf::ExternallyManagedObject<T, O>is the generic owner/inner wrapper: it usesManuallyDrop, invokesdestroyexactly once, and retains the owner until that call.DeviceObject<T>exposesdevice(),handle()forCopyhandles, and optionalname(). It only implementsDeviceOwned<T>whenTis a Vulkan handle.Queue::with_lockedserializes calls on a queue;DeviceQueues::family_indicesdeduplicates family IDs for resource-sharing modes.Fence::new_dedicatedandSemaphore::new_dedicatedcreate per-object device-owned primitives;from_poolconstructors create pooled sync objects.FenceFuture::blockwaits then resets. Polling an unsignaled future schedules a blocking waiter throughSyncThreadpool; the pool's worker count is bounded by its configured maximum.
Invariants and gotchas
asdfis the literal legacy module name forDeviceObjectand its traits. Do not silently rename paths; generated/client code refers to these APIs.- Vulkan child handles left bare in pools at device teardown are implicitly released by
vkDestroyDevice; pool entries must not independently retain device-owning wrappers. _drop: DeviceDropis the private shutdown path: it waits for the device to idle, exports and destroys the pipeline cache topipeline_cache.bin, then destroys the Vulkan device. The cache is also exposed as a copyable wrapper throughDevicePoolsfor pipeline creation.Devicecloning is cheap and prolongs the full device lifetime; any liveDeviceObject,PoolObject, sync wrapper, or other owner retaining aDevicecan delay shutdown.- Device-owned wrapper structs live outside
DeviceInnerand its pools. Pools hold bare handles, neverDeviceObjectorPoolObjectwrappers. - Keep enabled-feature state distinct from adapter-supported feature queries:
enabled_features()records what the logical device was created with, whilefeatures()is support information and must not be treated as enabled capability. Pool::device()upgrades itsWeakand panics if the pool outlives its device; don't retain pools independently.PoolObjectusesManuallyDropto transfer its inner handle toRecycleexactly once. NewRecycleimplementations must route to the matching pool and handle type.- The derive parser requires a named-field struct with exactly one
#[device]field and does not add generic bounds itself; generic field types must satisfyDeviceRefthrough the input declaration. Its generated paths require the crate self-alias inlib.rs. vk::SemaphoreandSemaphoreInnerhave destruction implementations for dedicated ownership; pooled semaphore ownership instead recyclesSemaphoreInnerinto a kind-specific pool.vk::Buffer,vk::Image,vk::ImageView, descriptor/pipeline objects, swapchains, semaphores, and GPU allocations have concrete destruction paths; inspectdevice.rsandsync.rsbefore introducing another handle wrapper.- Queue
lock()/unlock()andDeviceInner::lock_queues/unlock_queuesare unsafe paired operations. PreferQueue::with_lockedorwait_idleunless the caller can uphold their contracts. Instanceshares its raw Vulkan instance throughArc;DebugUtilsowns and destroys its messenger. Device references retain theirInstance.superset_ofcompares requested-true flags witha != b, which looks inverted for a superset test — verify before relying on adapter feature selection.- Physical-device feature/property queries chain Vulkan 1.1/1.2/1.3 structures and optional mesh-shader data without checking each version's availability; property querying marks beyond-1.0 probing as not yet fallible.
choose_adapter_defaultignores its optional raw surface argument (TODO); it does not test surface compatibility.- Instance/queue selection accepts
RawDisplayHandle; surface capability lookup uses a rawvk::SurfaceKHR. This crate has norenderer::swapchain::Surfacedependency. queue::QueueFlagsanddevice::QueueFlagsintentionally serve different purposes. Keep imports qualified when adding queue-family selection logic.PipelineCachepersistence uses a single working-directory path and one cache record. Changes to the key or file header should account for compatibility with existing cache files.- Logical-device setup always includes the swapchain extension in its required extension list, regardless of the caller's extra requested extensions.
- Queue-family selection uses the optional display handle to query presentation support and currently asserts that its chosen graphics family is present-capable.
- Extension selectors and feature checks drive selection; do not infer that every optional extension is enabled just because a loader type exists.
Instance::query_presentation_supporthandles platform-specific display variants and has an unimplemented XCB branch.DebugUtilsis stored insideInstanceInner; its destructor destroys the messenger, so retain the loader/messenger for the instance lifetime.DeviceObject's concrete destruction impls are split betweendevice.rsandsync.rs; search both before adding a duplicate or assuming a type has an implementation.PhysicalDeviceInfoconstruction can discard adapters that fail property queries;Instance::choose_adapterfilters failed exposures before sorting.DeviceQueueInfos::retrieve_queuesreuses the graphics queue handle for absent compute or transfer roles. These aliases share the sameQueueInnerlock.QueueFamily::granularityuses the source spellingTransferGranuality; avoid renaming it as unrelated cleanup.sync::Semaphore::from_poolsignals pooled timeline semaphores to the provided value but does not reset them to zero first; ensure requested timeline value semantics remain valid on reuse.- Fence waiters wake on successful waits, log and stop on non-timeout errors, and can time out of the worker pool after its idle duration.
PipelineCacheuses MD5 only to derive a local adapter key; this is a cache identity, not a security mechanism.- Device-level extension loaders are constructed only for extensions recorded as enabled; callers should check optional loaders such as
device_extensions.swapchainbefore use. Device::PartialEqcomparesArcidentity, not raw Vulkan handles or physical-device identity.Instance::get_adapter_surface_capabilitiesreceives the surface loader separately from the raw handle and returns format/mode data alongside generic capabilities.- In debug builds
DebugNamestores its string; in non-debug builds its dereference returns a placeholder and does not retain the supplied name.debug_name_objectassigns names through debug utils. - GPU allocation free is routed through
DeviceObject<GpuAllocation>into the allocator mutex. Do not call allocator free on an allocation after ownership has moved or dropped. ExternallyManagedObject<Device>must destroy each owned handle once. Thevk::CommandBufferimplementation is intentionally a no-op: its outer command-buffer wrapper frees it through its command pool.vk::CommandPoolitself is really destroyed.- Device handle types are hand-written around
DeviceObjectwithDeviceOwnedimplementations where needed; the olddefine_device_owned_handle!macro no longer exists.
Conventions
- Nightly crate features:
debug_closure_helpers,slice_partition_dedup. - Keep unsafe Vulkan destruction and queue locking contracts explicit; inspect each trait's safety requirements when adding implementations.
- Do not commit changes. Verify with
cargo check --workspaceandcargo test -p renderer_devicefrom the workspace root. - Keep
renderer_deviceindependent of higher-level render resource/swapchain wrapper types; public raw Vulkan handles and capability records form this crate's boundary.
Change guidance
- A new device-owned Vulkan handle needs a concrete
ExternallyManagedObject<Device>implementation before it can be wrapped inDeviceObject<T>. - Keep
DeviceOwned<T>aligned with its wrapper:device()returns the strong owner used for destruction, andhandle()returns the wrapped raw handle. - A pooled type needs both
Pooled::create_from_poolandRecycle::recycle, a matching inline entry inDevicePools, and initialization from itsWeak<DeviceInner>. - Keep dedicated and pooled sync paths semantically separate. Reused fences may require reset; timeline semaphore reuse must honor counter values.
DeviceObject::new_debug_namedrequiresDebugNameable; useDeviceObject::newwhen no valid debug naming route exists.- Never put
DeviceorArc<Device>inside a value stored inline inDeviceInnerunless the ownership cycle is deliberately redesigned. - Raw Vulkan calls must use the instance/device associated with their handle. Document synchronization and lifetime obligations when exposing raw handles.
- New queue operations must use
Queue::with_lockedor explicitly document their external-synchronization safety contract. - When adding required features/extensions, update adapter support checks, physical-device queries, and logical-device enablement together.
required_extensionswarns when an additional requested device extension is unsupported and always includes swapchain; warnings are not a hard-failure policy.- Keep platform surface creation out of adapter APIs; selection/capability methods use raw Vulkan surface and display handles.
- Pipeline cache data is an optimization: load failures fall back to an empty cache; export/write failures are logged during teardown.
- Preserve
extern crate self as renderer_device: proc-macro expansion uses this absolute path inside crate tests as well as downstream code. - The derive parser's diagnostics require named-field structs and exactly one
#[device]field. Do not rely on the incomplete tuple-field branch. get_available_extensionsignores failed per-layer extension enumeration results; extension compatibility otherwise compares names and minimum spec versions.- On
Instance/Deviceteardown changes, inspect explicitDropimplementations and field drop order, not only local wrapper drops.