#[cfg(feature = "vulkan")]
pub(crate) mod vulkan;
#[cfg(all(feature = "dx12", target_os = "windows"))]
pub(crate) mod dx12;
pub(crate) mod mock;
#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) mod metal;
#[cfg(feature = "webgpu")]
pub(crate) mod webgpu;
#[cfg(feature = "cuda")]
pub(crate) mod cuda;
pub(crate) use crate::device::{AdapterInfo, BufferHeapStats, TextureHeapStats, VideoMemoryInfo};
pub(crate) use crate::handles::{
BufferHandle, ComputePipelineHandle, ContextHandle, DeviceHandle, PipelineHandle, RenderTargetHandle,
SamplerHandle, ShaderHandle, SurfaceHandle, SwapchainImageHandle, TextureHandle,
};
pub(crate) use crate::texture::TextureCopyFootprint;
pub(crate) mod shared;
pub(crate) mod submission_worker;
pub(crate) mod host_sidecar;
#[cfg(any(feature = "vulkan", all(feature = "dx12", target_os = "windows")))]
pub(crate) mod signal_fence;
use crate::types::{
BackendType, BufferFlags, BufferKind, DepthFormat, DepthStencilState, IndexFormat, PresentMode, PrimitiveTopology,
ResourceAccess, ResourceHandle, SamplerDesc, TextureFlags, TextureFormat, TextureKind, VertexBufferLayout,
};
use anyhow::Result;
use std::sync::Arc;
#[cfg(any(feature = "vulkan", all(feature = "metal", target_os = "macos")))]
#[must_use]
pub(crate) fn goldy_validation_enabled() -> bool {
crate::validation_env::gpu_api_validation_enabled()
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
use crate::types::ResourceCategory;
#[cfg(all(feature = "dx12", target_os = "windows"))]
use crate::types::BindlessSlotKind;
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
#[inline]
pub(crate) fn with_layout_validation<F>(f: F) -> Result<()>
where
F: FnOnce() -> Result<()>,
{
if crate::slang::layout_validation_enabled() {
f()
} else {
Ok(())
}
}
#[cfg(all(feature = "dx12", target_os = "windows"))]
pub(crate) fn validate_bindless_slot_kinds(
indices: &[u32],
expectations: &[Option<BindlessSlotKind>],
mut resolve: impl FnMut(u32) -> Option<BindlessSlotKind>,
shader_name: &str,
) -> Result<()> {
if expectations.is_empty() {
return Ok(());
}
let mut mismatches: Vec<String> = Vec::new();
for (slot, &index) in indices.iter().enumerate() {
let Some(expected) = expectations.get(slot).copied().flatten() else {
continue;
};
let Some(actual) = resolve(index) else {
continue;
};
if actual != expected {
mismatches.push(format!(
"slot {slot}: shader expects {expected} bindless slot but index {index} resolves to {actual}",
expected = expected.name(),
actual = actual.name(),
));
}
}
if mismatches.is_empty() {
Ok(())
} else {
anyhow::bail!(
"bindless SRV/UAV mismatch in shader `{shader_name}`:\n {}\n\
Hint: `Scattered<T>` / `StorageBuffer<T>` need `ResourceAccess::ReadWrite` or `Write` \
(UAV index); `BufRO<T>` needs `ResourceAccess::Read` (SRV index). \
Prefer `bind_resources(&[&buf])` or `Buffer::handle(access)` over raw indices.",
mismatches.join("\n ")
);
}
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
pub(crate) fn validate_typed_push_constants(
handles: &[ResourceHandle],
expectations: &[Option<ResourceCategory>],
shader_name: &str,
) -> Result<()> {
let mut mismatches: Vec<String> = Vec::new();
for (slot, handle) in handles.iter().enumerate() {
let Some(expected) = expectations.get(slot).copied().flatten() else {
continue;
};
if !handle.category().is_compatible_with(expected) {
mismatches.push(format!(
"slot {slot}: shader expects `{}` but got `{}` handle (index {})",
expected.name(),
handle.category().name(),
handle.index()
));
}
}
if mismatches.is_empty() {
Ok(())
} else {
anyhow::bail!(
"push-constant category mismatch in shader `{shader_name}`:\n {}\n\
Hint: use `Buffer::handle(access)` / `Texture::handle(access)` \
rather than raw backend indices so the resource's category flows \
through to the push-constant setter.",
mismatches.join("\n ")
);
}
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
pub(crate) fn validate_binding_strides(
buffer_strides: &[Option<u32>],
expected: &[Option<u32>],
shader_name: &str,
) -> Result<()> {
let mut mismatches: Vec<String> = Vec::new();
for (slot, actual) in buffer_strides.iter().enumerate() {
let Some(exp) = expected.get(slot).copied().flatten() else {
continue;
};
let Some(act) = actual else {
continue;
};
if *act != exp {
mismatches.push(format!(
"slot {slot}: shader expects element stride {exp} but buffer has {act}"
));
}
}
if mismatches.is_empty() {
Ok(())
} else {
anyhow::bail!(
"buffer element-stride mismatch in shader `{shader_name}`:\n {}\n\
Hint: for Scattered<T> / BufRO<T> parameters, ensure the buffer's element_stride \
matches sizeof(T) as declared in the shader.\n\
For Broadcast (constant-buffer) parameters, use acquire_buffer_sized::<T>() where T \
exactly matches the shader struct (e.g. #[repr(C)] with the same fields and order).",
mismatches.join("\n ")
);
}
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
pub(crate) fn validate_raw_binding_strides(
indices: &[u32],
categories: &[Option<crate::types::ResourceCategory>],
expected: &[Option<u32>],
mut resolve_stride: impl FnMut(u32, crate::types::ResourceCategory) -> Option<u32>,
shader_name: &str,
) -> Result<()> {
if expected.is_empty() {
return Ok(());
}
let mut actual: Vec<Option<u32>> = vec![None; expected.len()];
let mut missing_slots: Vec<usize> = Vec::new();
for (slot, exp) in expected.iter().enumerate() {
if exp.is_none() {
continue;
}
let Some(cat) = categories.get(slot).and_then(|c| *c) else {
continue;
};
if !matches!(
cat,
crate::types::ResourceCategory::Scattered | crate::types::ResourceCategory::Broadcast
) {
continue;
}
match indices.get(slot) {
Some(&idx) => actual[slot] = resolve_stride(idx, cat),
None => missing_slots.push(slot),
}
}
if !missing_slots.is_empty() {
let slots: Vec<String> = missing_slots.iter().map(|s| s.to_string()).collect();
anyhow::bail!(
"bind_resources_raw for shader `{shader_name}` is missing indices for \
slot(s) {}: shader has {} reflected binding slot(s) but only {} index/indices \
were provided.\n\
Hint: pass one raw bindless index per Scattered/Broadcast parameter in the \
shader signature, in declaration order.",
slots.join(", "),
expected.len(),
indices.len(),
);
}
validate_binding_strides(&actual, expected, shader_name)
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
pub(crate) fn validate_render_pass_bind_resources<F, G>(
commands: &[RenderCommand],
mut pipeline_strides: F,
mut buffer_stride: G,
) -> Result<()>
where
F: FnMut(PipelineHandle) -> Option<(Vec<Option<u32>>, String)>,
G: FnMut(BufferHandle) -> Option<u32>,
{
let mut current_pipeline: Option<PipelineHandle> = None;
for cmd in commands {
match cmd {
RenderCommand::SetPipeline(h) => current_pipeline = Some(*h),
RenderCommand::BindResources { buffers } => {
if let Some(ph) = current_pipeline {
if let Some((expected, name)) = pipeline_strides(ph) {
if expected.is_empty() {
continue;
}
let actual: Vec<Option<u32>> = buffers.iter().map(|h| buffer_stride(*h)).collect();
validate_binding_strides(&actual, &expected, &name)?;
}
}
}
_ => {}
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct FrameToken {
pub surface: SurfaceHandle,
pub image: SwapchainImageHandle,
pub context: ContextHandle,
pub frame_slot: u32,
pub present_slot: u32,
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub(crate) enum RenderCommand {
ClearDepth(f32),
SetPipeline(PipelineHandle),
SetVertexBuffer {
slot: u32,
buffer: BufferHandle,
offset: u64,
},
SetIndexBuffer {
buffer: BufferHandle,
offset: u64,
format: IndexFormat,
},
BindResources { buffers: Vec<BufferHandle> },
BindResourcesRaw {
indices: Vec<u32>,
user: Vec<u32>,
frame_table_base: u32,
},
BindResourcesTyped { handles: Vec<ResourceHandle> },
Draw {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
},
DrawIndexed {
index_count: u32,
instance_count: u32,
first_index: u32,
base_vertex: i32,
first_instance: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeferredHostWrite {
pub buffer: BufferHandle,
pub offset: u64,
pub data: Arc<[u8]>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SubmitSync {
pub prologue: crate::task_graph::BarrierSet,
pub waits: Vec<crate::timeline::Epoch>,
pub cpu_waits: Vec<crate::timeline::Epoch>,
pub host_observed_waits: Vec<crate::timeline::Epoch>,
pub deferred_host_writes: Vec<DeferredHostWrite>,
}
impl SubmitSync {
pub fn is_empty(&self) -> bool {
self.prologue.is_empty()
&& self.waits.is_empty()
&& self.cpu_waits.is_empty()
&& self.host_observed_waits.is_empty()
&& self.deferred_host_writes.is_empty()
}
pub fn merge_queue_waits(&mut self, extra: &[crate::timeline::Epoch]) {
crate::backend::host_sidecar::merge_epochs(&mut self.waits, extra);
}
pub fn merge_host_observed_waits(&mut self, extra: &[crate::timeline::Epoch]) {
crate::backend::host_sidecar::merge_epochs(&mut self.host_observed_waits, extra);
}
#[cfg_attr(not(any(test, feature = "vulkan")), allow(dead_code))]
pub fn use_legacy_acquire(&self) -> bool {
false
}
#[cfg_attr(not(any(test, feature = "vulkan")), allow(dead_code))]
pub fn use_legacy_acquire_from(sync: Option<&SubmitSync>) -> bool {
match sync {
None => true,
Some(s) => s.use_legacy_acquire(),
}
}
}
#[cfg(test)]
mod submit_sync_tests {
use super::SubmitSync;
#[test]
fn use_legacy_acquire_from_none_means_legacy() {
assert!(SubmitSync::use_legacy_acquire_from(None));
}
#[test]
fn use_legacy_acquire_from_some_means_scoped() {
assert!(!SubmitSync::use_legacy_acquire_from(Some(&SubmitSync::default())));
}
}
pub(crate) fn commands_with_sync_prologue(commands: &[GpuCommand], sync: Option<&SubmitSync>) -> Vec<GpuCommand> {
if let Some(s) = sync {
if !s.prologue.is_empty() {
return crate::task_graph::cross_submit::prepend_prologue(commands, &s.prologue);
}
}
commands.to_vec()
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum GpuCommand {
SetPipeline(ComputePipelineHandle),
BindResourcesRaw {
indices: Vec<u32>,
user: Vec<u32>,
frame_table_base: u32,
},
Dispatch {
label: Option<&'static str>,
workgroups_x: u32,
workgroups_y: u32,
workgroups_z: u32,
},
DispatchIndirect {
label: Option<&'static str>,
buffer: BufferHandle,
offset: u64,
},
ClearBuffer {
buffer: BufferHandle,
offset: u64,
size: u64,
},
WriteBuffer {
buffer: BufferHandle,
offset: u64,
data: Arc<[u8]>,
},
WriteTexture {
texture: TextureHandle,
data: Arc<[u8]>,
width: u32,
height: u32,
},
WriteTextureRegion {
texture: TextureHandle,
x: u32,
y: u32,
width: u32,
height: u32,
data: Arc<[u8]>,
},
CopyTexture { src: TextureHandle, dst: TextureHandle },
CopyRenderTarget {
src: RenderTargetHandle,
dst: TextureHandle,
},
CopyBuffer {
src: BufferHandle,
src_offset: u64,
dst: BufferHandle,
dst_offset: u64,
size: u64,
},
CopyBufferToTexture {
src: BufferHandle,
src_offset: u64,
src_row_pitch: u32,
dst: TextureHandle,
x: u32,
y: u32,
width: u32,
height: u32,
},
CopyTextureToReadback {
src: TextureHandle,
dst: BufferHandle,
layout: TextureCopyFootprint,
},
DispatchBatch {
label: Option<&'static str>,
arg_data: Arc<[u8]>,
count: u32,
},
FrameTableStaging { data: std::sync::Arc<[u32]> },
ResourceBarrier {
buffers: Vec<(BufferHandle, crate::task_graph::BarrierUsage)>,
textures: Vec<(TextureHandle, crate::task_graph::BarrierUsage)>,
},
}
#[derive(Debug, Clone)]
pub(crate) enum GraphCommand {
Compute(GpuCommand),
Render {
target: RenderTargetHandle,
color_load: crate::types::TargetLoad,
commands: Vec<RenderCommand>,
},
}
pub(crate) trait TimelineBlockingWait: Send {
fn block(self: Box<Self>) -> Result<()>;
fn block_timeout(self: Box<Self>, _timeout_ms: u32) -> Result<bool> {
self.block()?;
Ok(true)
}
}
#[doc(hidden)]
pub(crate) trait ContextDestroyHandle: Send {
fn wait(&self) -> Result<()>;
fn finish(self: Box<Self>) -> Result<()>;
}
pub(crate) fn run_context_destroy(handle: Box<dyn ContextDestroyHandle>) {
let _ = handle.wait();
let _ = handle.finish();
}
#[cfg(any(
test,
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
))]
pub(crate) fn destroy_context_mut(backend: &mut dyn GpuBackend, ctx: ContextHandle) {
if let Some(handle) = backend.detach_context_for_destroy(ctx) {
run_context_destroy(handle);
}
}
pub(crate) fn destroy_context(backend: &std::sync::Arc<std::sync::Mutex<Box<dyn GpuBackend>>>, ctx: ContextHandle) {
if let Some(handle) = {
let mut guard = backend.lock().unwrap();
guard.detach_context_for_destroy(ctx)
} {
run_context_destroy(handle);
}
}
#[doc(hidden)]
pub(crate) trait ContextDeferredDeletionFlush: Send + Sync {
fn flush(&self);
}
#[doc(hidden)]
pub(crate) trait ContextGpuProgress: Send + Sync {
fn gpu_progress(&self) -> crate::timeline::TimelineValue;
}
#[doc(hidden)]
pub(crate) trait ContextReclamationScope: Send + Sync {
fn set_epoch(&self, epoch: Option<crate::timeline::TimelineValue>);
}
pub(crate) struct NoOpReclamationScope;
impl ContextReclamationScope for NoOpReclamationScope {
fn set_epoch(&self, _epoch: Option<crate::timeline::TimelineValue>) {}
}
pub(crate) struct NoOpDeferredDeletionFlush;
impl ContextDeferredDeletionFlush for NoOpDeferredDeletionFlush {
fn flush(&self) {}
}
#[allow(dead_code)] pub(crate) struct PresentFinishState {
pub frame: FrameToken,
pub return_fence: crate::timeline::TimelineValue,
pub scratch_texture: Option<TextureHandle>,
pub scratch_layout_updated: bool,
pub present_timeline: crate::timeline::TimelineValue,
pub copy_timeline: Option<crate::timeline::TimelineValue>,
pub frame_compute_timeline: Option<crate::timeline::TimelineValue>,
pub signal_timeline: Option<crate::timeline::TimelineValue>,
pub render_pass_submitted: bool,
pub present_ok: bool,
}
pub(crate) trait PresentGpuWork: Send {
fn run(self: Box<Self>) -> Result<PresentFinishState>;
}
pub(crate) trait ContextSubmitSession: Send + Sync {
fn separate_graphics_queue(&self) -> bool {
false
}
fn device_queue_owner(&self, _ctx: ContextHandle) -> Option<ContextHandle> {
None
}
fn retains_present_partitions(&self) -> bool;
fn submit_standalone(
&self,
ctx: ContextHandle,
commands: &[GpuCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue>;
fn submit_graph(
&self,
ctx: ContextHandle,
commands: &[GraphCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue>;
fn submit_graph_and_retain(
&self,
ctx: ContextHandle,
commands: &[GraphCommand],
key: u64,
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue>;
fn try_resubmit_retained(
&self,
ctx: ContextHandle,
key: u64,
sync: Option<&SubmitSync>,
) -> Result<Option<crate::timeline::TimelineValue>>;
fn evict_retained(&self, ctx: ContextHandle, key: u64);
}
pub(crate) trait GpuBackendSubmitSession {
fn clone_context_submit_session(
&self,
ctx: ContextHandle,
backend: std::sync::Arc<std::sync::Mutex<Box<dyn GpuBackend>>>,
) -> std::sync::Arc<dyn ContextSubmitSession>;
}
pub(crate) struct LockedSubmitSession {
backend: std::sync::Arc<std::sync::Mutex<Box<dyn GpuBackend>>>,
backend_type: BackendType,
}
impl LockedSubmitSession {
pub fn with_backend_type(
backend: std::sync::Arc<std::sync::Mutex<Box<dyn GpuBackend>>>,
backend_type: BackendType,
) -> std::sync::Arc<dyn ContextSubmitSession> {
std::sync::Arc::new(Self { backend, backend_type })
}
}
impl ContextSubmitSession for LockedSubmitSession {
fn retains_present_partitions(&self) -> bool {
!matches!(self.backend_type, crate::types::BackendType::Metal)
}
fn submit_standalone(
&self,
ctx: ContextHandle,
commands: &[GpuCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue> {
let mut guard = self.backend.lock().unwrap();
let tv = guard.submit_standalone(ctx, commands, sync)?;
drop(guard);
Ok(tv)
}
fn submit_graph(
&self,
ctx: ContextHandle,
commands: &[GraphCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue> {
self.backend.lock().unwrap().submit_graph(ctx, commands, sync)
}
fn submit_graph_and_retain(
&self,
ctx: ContextHandle,
commands: &[GraphCommand],
key: u64,
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue> {
self.backend
.lock()
.unwrap()
.submit_graph_and_retain(ctx, commands, key, sync)
}
fn try_resubmit_retained(
&self,
ctx: ContextHandle,
key: u64,
sync: Option<&SubmitSync>,
) -> Result<Option<crate::timeline::TimelineValue>> {
self.backend.lock().unwrap().try_resubmit_retained(ctx, key, sync)
}
fn evict_retained(&self, ctx: ContextHandle, key: u64) {
self.backend.lock().unwrap().evict_retained(ctx, key);
}
}
pub(crate) trait GpuBackendPresentSplit {
fn take_present_gpu_work(
&mut self,
frame: FrameToken,
submit_tv: crate::timeline::TimelineValue,
) -> Result<Box<dyn PresentGpuWork>>;
fn finish_present(
&mut self,
finish: PresentFinishState,
submit_tv: crate::timeline::TimelineValue,
) -> Result<crate::timeline::TimelineValue>;
}
pub(crate) trait GpuBackendTimelineWait {
fn take_timeline_submission_epoch_wait(
&self,
ctx: ContextHandle,
value: crate::timeline::TimelineValue,
) -> Result<Option<submission_worker::SubmissionEpochWait>>;
fn take_timeline_blocking_wait(
&self,
ctx: ContextHandle,
value: crate::timeline::TimelineValue,
) -> Result<Option<Box<dyn TimelineBlockingWait>>>;
fn finish_timeline_wait(&mut self, ctx: ContextHandle, value: crate::timeline::TimelineValue) -> Result<()>;
}
#[allow(private_bounds)]
pub(crate) trait GpuBackend:
Send + Sync + GpuBackendTimelineWait + GpuBackendPresentSplit + GpuBackendSubmitSession
{
#[doc(hidden)]
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
fn backend_type(&self) -> BackendType;
fn enumerate_adapters(&self) -> Vec<AdapterInfo>;
fn adapter_capabilities(&self, adapter_id: u32) -> crate::device::DeviceCapabilities {
let _ = adapter_id;
crate::device::DeviceCapabilities::default()
}
fn create_device(&mut self, adapter_id: u32) -> Result<DeviceHandle>;
fn destroy_device(&mut self, device: DeviceHandle);
fn is_device_valid(&self, device: DeviceHandle) -> bool;
fn device_wait_idle(&mut self, device: DeviceHandle) -> Result<()>;
fn create_context(&mut self, device: DeviceHandle) -> Result<ContextHandle>;
#[doc(hidden)]
fn detach_context_for_destroy(&mut self, ctx: ContextHandle) -> Option<Box<dyn ContextDestroyHandle>>;
#[doc(hidden)]
fn clone_context_deletion_flush(
&self,
ctx: ContextHandle,
) -> Option<std::sync::Arc<dyn ContextDeferredDeletionFlush>>;
#[doc(hidden)]
fn clone_context_gpu_progress(&self, ctx: ContextHandle) -> Option<std::sync::Arc<dyn ContextGpuProgress>> {
let _ = ctx;
None
}
#[doc(hidden)]
fn clone_context_reclamation_scope(&self, ctx: ContextHandle) -> std::sync::Arc<dyn ContextReclamationScope> {
let _ = ctx;
std::sync::Arc::new(NoOpReclamationScope)
}
fn is_device_lost(&self, _device: DeviceHandle) -> bool {
false
}
fn query_video_memory(&self, _device: DeviceHandle) -> Option<VideoMemoryInfo> {
None
}
fn create_buffer(
&mut self,
device: DeviceHandle,
size: u64,
access: BufferKind,
element_stride: Option<u32>,
flags: BufferFlags,
) -> Result<BufferHandle>;
fn create_buffer_with_capacity(
&mut self,
device: DeviceHandle,
initial_size: u64,
capacity: u64,
access: BufferKind,
element_stride: Option<u32>,
flags: BufferFlags,
) -> Result<(BufferHandle, u64)> {
let _ = capacity;
let handle = self.create_buffer(device, initial_size, access, element_stride, flags)?;
Ok((handle, initial_size))
}
fn destroy_buffer(&mut self, buffer: BufferHandle);
fn write_buffer(&mut self, buffer: BufferHandle, offset: u64, data: &[u8]) -> Result<()>;
fn alloc_readback_buffer(&mut self, device: DeviceHandle, size: u64) -> Result<BufferHandle>;
fn read_readback_buffer(&self, buffer: BufferHandle, output: &mut [u8]) -> Result<()>;
fn free_readback_buffer(&mut self, buffer: BufferHandle);
fn query_texture_copy_footprint(
&self,
device: DeviceHandle,
width: u32,
height: u32,
format: TextureFormat,
) -> Result<TextureCopyFootprint>;
fn alloc_texture_readback_staging(
&mut self,
device: DeviceHandle,
layout: TextureCopyFootprint,
) -> Result<BufferHandle>;
fn read_texture_readback_staging(
&self,
buffer: BufferHandle,
layout: TextureCopyFootprint,
output: &mut [u8],
) -> Result<()>;
fn texture_copy_retention_tag(&self, texture: TextureHandle) -> u64;
#[doc(hidden)]
#[cfg(test)]
fn test_readback_alloc_count(&self) -> usize {
let _ = self;
0
}
#[doc(hidden)]
#[cfg(test)]
fn test_readback_free_count(&self) -> usize {
let _ = self;
0
}
#[doc(hidden)]
#[cfg(test)]
fn test_surface_present_count(&self) -> usize {
let _ = self;
0
}
fn clear_buffer(&mut self, device: DeviceHandle, buffer: BufferHandle, offset: u64, size: u64) -> Result<()>;
fn buffer_size(&self, buffer: BufferHandle) -> u64;
fn buffer_capacity(&self, buffer: BufferHandle) -> u64 {
self.buffer_size(buffer)
}
fn set_buffer_logical_size(
&mut self,
device: DeviceHandle,
buffer: BufferHandle,
new_logical_size: u64,
) -> Result<()>;
fn hint_buffer_unused_above(&mut self, buffer: BufferHandle, offset: u64) {
let _ = (buffer, offset);
}
fn buffer_bindless_index(&self, buffer: BufferHandle) -> Option<u32>;
fn buffer_bindless_srv_index(&self, buffer: BufferHandle) -> Option<u32>;
fn create_buffer_view(
&mut self,
parent: BufferHandle,
offset: u64,
size: u64,
element_stride: Option<u32>,
) -> Result<BufferHandle>;
fn resize_buffer(
&mut self,
device: DeviceHandle,
buffer: BufferHandle,
new_size: u64,
preserve_contents: bool,
) -> Result<()>;
fn create_shader_with_paths(
&mut self,
device: DeviceHandle,
slang_source: &str,
search_paths: &[&str],
defines: &[(&str, &str)],
optimization_level: crate::types::OptimizationLevel,
) -> Result<ShaderHandle>;
fn create_shader_with_checks(
&mut self,
device: DeviceHandle,
slang_source: &str,
search_paths: &[&str],
defines: &[(&str, &str)],
optimization_level: crate::types::OptimizationLevel,
layout_checks: Vec<crate::slang::OwnedLayoutCheck>,
) -> Result<ShaderHandle> {
if layout_checks.is_empty() {
self.create_shader_with_paths(device, slang_source, search_paths, defines, optimization_level)
} else {
anyhow::bail!("Layout validation requires the Vulkan, DX12, or Metal backend")
}
}
fn destroy_shader(&mut self, shader: ShaderHandle);
fn create_pipeline(
&mut self,
device: DeviceHandle,
vertex_shader: ShaderHandle,
fragment_shader: ShaderHandle,
vertex_layout: &VertexBufferLayout,
topology: PrimitiveTopology,
target_format: TextureFormat,
) -> Result<PipelineHandle>;
fn destroy_pipeline(&mut self, pipeline: PipelineHandle);
#[allow(clippy::too_many_arguments)]
fn create_pipeline_with_depth(
&mut self,
device: DeviceHandle,
vertex_shader: ShaderHandle,
fragment_shader: ShaderHandle,
vertex_layout: &VertexBufferLayout,
topology: PrimitiveTopology,
target_format: TextureFormat,
depth_stencil: Option<&DepthStencilState>,
) -> Result<PipelineHandle>;
fn create_render_target_with_depth(
&mut self,
device: DeviceHandle,
width: u32,
height: u32,
color_format: TextureFormat,
depth_format: Option<DepthFormat>,
) -> Result<RenderTargetHandle>;
fn render_to_target(
&mut self,
device: DeviceHandle,
target: RenderTargetHandle,
color_load: crate::types::TargetLoad,
commands: &[RenderCommand],
) -> Result<()>;
fn create_texture(
&mut self,
device: DeviceHandle,
width: u32,
height: u32,
format: TextureFormat,
access: TextureKind,
flags: TextureFlags,
) -> Result<TextureHandle>;
fn write_texture(&mut self, texture: TextureHandle, data: &[u8], width: u32, height: u32) -> Result<()>;
fn write_texture_region(
&mut self,
texture: TextureHandle,
x: u32,
y: u32,
width: u32,
height: u32,
data: &[u8],
) -> Result<()>;
fn destroy_texture(&mut self, texture: TextureHandle);
fn set_texture_debug_name(&mut self, _handle: TextureHandle, _name: &str) {}
fn texture_bindless_index(&self, texture: TextureHandle) -> Option<u32>;
fn texture_bindless_sampled_index(&self, texture: TextureHandle) -> Option<u32>;
fn create_sampler(&mut self, device: DeviceHandle, desc: &SamplerDesc) -> Result<SamplerHandle>;
fn destroy_sampler(&mut self, sampler: SamplerHandle);
fn sampler_bindless_index(&self, sampler: SamplerHandle) -> Option<u32>;
fn create_surface(
&mut self,
device: DeviceHandle,
window: &dyn raw_window_handle::HasWindowHandle,
display: &dyn raw_window_handle::HasDisplayHandle,
depth_format: Option<DepthFormat>,
) -> Result<SurfaceHandle>;
fn destroy_surface(&mut self, surface: SurfaceHandle);
fn surface_resize(&mut self, surface: SurfaceHandle, width: u32, height: u32) -> Result<()>;
fn surface_size(&self, surface: SurfaceHandle) -> (u32, u32);
fn surface_format(&self, surface: SurfaceHandle) -> TextureFormat;
fn surface_set_present_mode(&mut self, _surface: SurfaceHandle, _mode: PresentMode) -> Result<()> {
Ok(())
}
fn gpu_progress(&self, ctx: ContextHandle) -> crate::timeline::TimelineValue;
fn device_timeline_retired(&self, device: DeviceHandle) -> crate::timeline::TimelineValue;
fn device_wait_until(&mut self, device: DeviceHandle, value: crate::timeline::TimelineValue) -> Result<()>;
fn poll_signals(
&mut self,
ctx: ContextHandle,
progress: crate::timeline::TimelineValue,
) -> Vec<crate::signal::QueuedSignal>;
fn wait_until(&mut self, ctx: ContextHandle, value: crate::timeline::TimelineValue) -> Result<()> {
if let Some(wait) = self.take_timeline_blocking_wait(ctx, value)? {
wait.block()?;
}
self.finish_timeline_wait(ctx, value)
}
fn submit_standalone(
&mut self,
ctx: ContextHandle,
commands: &[GpuCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue>;
fn submit_graph(
&mut self,
ctx: ContextHandle,
commands: &[GraphCommand],
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue> {
let mut batch: Vec<GpuCommand> = Vec::new();
let mut last_tv = self.gpu_progress(ctx);
for cmd in commands {
match cmd {
GraphCommand::Compute(c) => batch.push(c.clone()),
GraphCommand::Render {
target,
color_load,
commands: render_cmds,
} => {
if !batch.is_empty() {
last_tv = self.submit_standalone(ctx, &batch, sync)?;
self.wait_until(ctx, last_tv)?;
batch.clear();
}
let device = self.context_device(ctx);
self.render_to_target(device, *target, *color_load, render_cmds)?;
last_tv = self.submit_standalone(ctx, &[], sync)?;
}
}
}
if !batch.is_empty() {
last_tv = self.submit_standalone(ctx, &batch, sync)?;
}
Ok(last_tv)
}
fn context_device(&self, ctx: ContextHandle) -> DeviceHandle;
fn submit_graph_and_retain(
&mut self,
ctx: ContextHandle,
commands: &[GraphCommand],
key: u64,
sync: Option<&SubmitSync>,
) -> Result<crate::timeline::TimelineValue> {
let _ = key;
self.submit_graph(ctx, commands, sync)
}
fn try_resubmit_retained(
&mut self,
ctx: ContextHandle,
key: u64,
sync: Option<&SubmitSync>,
) -> Result<Option<crate::timeline::TimelineValue>> {
let _ = (ctx, key, sync);
Ok(None)
}
fn evict_retained(&mut self, _ctx: ContextHandle, _key: u64) {}
fn begin_frame(&mut self, surface: SurfaceHandle, ctx: ContextHandle) -> Result<(FrameToken, TextureHandle)>;
fn submit_frame(&mut self, frame: &FrameToken) -> Result<crate::timeline::TimelineValue>;
fn create_compute_pipeline(
&mut self,
device: DeviceHandle,
compute_shader: ShaderHandle,
debug_name: Option<&str>,
) -> Result<ComputePipelineHandle>;
fn destroy_compute_pipeline(&mut self, pipeline: ComputePipelineHandle);
fn compute_pipeline_slot_access(&self, _pipeline: ComputePipelineHandle) -> Vec<Option<ResourceAccess>> {
Vec::new()
}
fn render_pipeline_slot_access(&self, _pipeline: PipelineHandle) -> Vec<Option<ResourceAccess>> {
Vec::new()
}
fn reset_buffer_heaps(&mut self, _device: DeviceHandle) {}
fn ensure_buffer_heap_capacity(&mut self, _device: DeviceHandle, _min_capacity: u64) {}
fn compact_overflow_heaps(&mut self, _device: DeviceHandle) {}
fn release_idle_shader_compiler(&mut self) {}
fn available_bindless_slots(&self, _device: DeviceHandle, _category: crate::types::ResourceCategory) -> u32 {
u32::MAX
}
fn max_bindless_slots_per_category(&self, _device: DeviceHandle, _category: crate::types::ResourceCategory) -> u32 {
u32::MAX
}
fn max_submission_contexts(&self, _device: DeviceHandle) -> u32 {
u32::MAX
}
#[doc(hidden)]
fn deferred_deletion_pending_count(&self, _ctx: ContextHandle) -> usize {
0
}
#[doc(hidden)]
fn device_deferred_deletion_pending_count(&self, _device: DeviceHandle) -> usize {
0
}
#[doc(hidden)]
fn buffer_heap_stats(&self, _device: DeviceHandle) -> Option<BufferHeapStats> {
None
}
#[doc(hidden)]
fn texture_heap_stats(&self, _device: DeviceHandle) -> Option<TextureHeapStats> {
None
}
#[doc(hidden)]
fn in_flight_command_buffer_count(&self, _ctx: ContextHandle) -> usize {
0
}
}
pub(crate) fn create_default_backend() -> Result<Box<dyn GpuBackend>> {
if let Ok(backend_str) = std::env::var("GOLDY_BACKEND") {
let backend_type = match backend_str.to_lowercase().as_str() {
"vulkan" | "vk" => BackendType::Vulkan,
"dx12" | "d3d12" | "directx" => BackendType::Dx12,
"metal" | "mtl" => BackendType::Metal,
"webgpu" | "wgpu" => BackendType::WebGpu,
"cuda" => BackendType::Cuda,
other => anyhow::bail!(
"Unknown GOLDY_BACKEND value '{}'. Valid options: vulkan, dx12, metal, webgpu, cuda",
other
),
};
tracing::info!("Using backend from GOLDY_BACKEND env var: {:?}", backend_type);
return create_backend(backend_type);
}
#[cfg(all(feature = "metal", target_os = "macos"))]
{
tracing::info!("Creating Metal backend");
Ok(Box::new(metal::MetalBackend::new()?))
}
#[cfg(all(
feature = "dx12",
target_os = "windows",
not(all(feature = "metal", target_os = "macos"))
))]
{
tracing::info!("Creating DX12 backend");
Ok(Box::new(dx12::Dx12Backend::new()?))
}
#[cfg(all(
feature = "vulkan",
not(all(feature = "dx12", target_os = "windows")),
not(all(feature = "metal", target_os = "macos"))
))]
{
tracing::info!("Creating Vulkan backend");
Ok(Box::new(vulkan::VulkanBackend::new()?))
}
#[cfg(not(any(
all(feature = "metal", target_os = "macos"),
all(feature = "dx12", target_os = "windows"),
feature = "vulkan"
)))]
{
anyhow::bail!("No GPU backend available - enable 'vulkan', 'dx12', or 'metal' feature")
}
}
pub(crate) fn create_shared_backend() -> Result<std::sync::Arc<std::sync::Mutex<Box<dyn GpuBackend>>>> {
use std::sync::{Arc, Mutex};
#[cfg(all(feature = "dx12", target_os = "windows"))]
{
let wants_dx12 = match std::env::var("GOLDY_BACKEND") {
Ok(v) => matches!(v.to_lowercase().as_str(), "dx12" | "d3d12" | "directx"),
Err(_) => true, };
if wants_dx12 {
return dx12::shared_backend();
}
}
let backend = create_default_backend()?;
Ok(Arc::new(Mutex::new(backend)))
}
pub(crate) fn create_backend(backend_type: BackendType) -> Result<Box<dyn GpuBackend>> {
match backend_type {
#[cfg(feature = "vulkan")]
BackendType::Vulkan => {
tracing::info!("Creating Vulkan backend");
Ok(Box::new(vulkan::VulkanBackend::new()?))
}
#[cfg(all(feature = "dx12", target_os = "windows"))]
BackendType::Dx12 => {
tracing::info!("Creating DX12 backend");
Ok(Box::new(dx12::Dx12Backend::new()?))
}
#[cfg(all(feature = "metal", target_os = "macos"))]
BackendType::Metal => {
tracing::info!("Creating Metal backend");
Ok(Box::new(metal::MetalBackend::new()?))
}
#[cfg(feature = "webgpu")]
BackendType::WebGpu => {
tracing::info!("Creating WebGPU backend (compute-only)");
Ok(Box::new(webgpu::WebGpuBackend::new()?))
}
#[cfg(feature = "cuda")]
BackendType::Cuda => {
tracing::info!("Creating CUDA backend (compute-only)");
Ok(Box::new(cuda::CudaBackend::new()?))
}
_ => anyhow::bail!("Backend {:?} not available on this platform", backend_type),
}
}
#[cfg(test)]
mod push_constant_validation_tests {
use super::validate_typed_push_constants;
use crate::types::{ResourceCategory, ResourceHandle};
#[test]
fn valid_categories_pass() {
let handles = vec![
ResourceHandle::new(ResourceCategory::Scattered, 0),
ResourceHandle::new(ResourceCategory::Broadcast, 1),
];
let expectations = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Broadcast)];
validate_typed_push_constants(&handles, &expectations, "test_shader").unwrap();
}
#[test]
fn none_expectations_are_skipped() {
let handles = vec![
ResourceHandle::new(ResourceCategory::Scattered, 0),
ResourceHandle::new(ResourceCategory::Texture, 1),
];
let expectations = vec![None, None];
validate_typed_push_constants(&handles, &expectations, "test_shader").unwrap();
}
#[test]
fn empty_expectations_passes_any_handles() {
let handles = vec![
ResourceHandle::new(ResourceCategory::Scattered, 5),
ResourceHandle::new(ResourceCategory::Sampler, 2),
];
validate_typed_push_constants(&handles, &[], "test_shader").unwrap();
}
#[test]
fn scattered_where_broadcast_expected_fails() {
let handles = vec![ResourceHandle::new(ResourceCategory::Scattered, 0)];
let expectations = vec![Some(ResourceCategory::Broadcast)];
let err = validate_typed_push_constants(&handles, &expectations, "my_shader")
.unwrap_err()
.to_string();
assert!(err.contains("slot 0"), "error should name the slot: {err}");
assert!(
err.contains("broadcast"),
"error should mention expected category: {err}"
);
assert!(err.contains("scattered"), "error should mention actual category: {err}");
}
#[test]
fn texture_where_scattered_expected_fails() {
let handles = vec![
ResourceHandle::new(ResourceCategory::Scattered, 0),
ResourceHandle::new(ResourceCategory::Texture, 3),
];
let expectations = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Scattered)];
let err = validate_typed_push_constants(&handles, &expectations, "compute_cs")
.unwrap_err()
.to_string();
assert!(err.contains("slot 1"), "error should name slot 1: {err}");
assert!(err.contains("texture"), "error should mention actual: {err}");
}
#[test]
fn multiple_mismatches_reported() {
let handles = vec![
ResourceHandle::new(ResourceCategory::Texture, 0),
ResourceHandle::new(ResourceCategory::Sampler, 1),
];
let expectations = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Broadcast)];
let err = validate_typed_push_constants(&handles, &expectations, "sh")
.unwrap_err()
.to_string();
assert!(err.contains("slot 0"), "should report slot 0: {err}");
assert!(err.contains("slot 1"), "should report slot 1: {err}");
}
}
#[cfg(all(test, feature = "dx12", target_os = "windows"))]
mod bindless_slot_validation_tests {
use super::validate_bindless_slot_kinds;
use crate::types::BindlessSlotKind;
#[test]
fn matching_srv_uav_passes() {
let expectations = vec![Some(BindlessSlotKind::StorageUav), Some(BindlessSlotKind::ReadOnlySrv)];
validate_bindless_slot_kinds(
&[10, 20],
&expectations,
|idx| {
Some(match idx {
10 => BindlessSlotKind::StorageUav,
20 => BindlessSlotKind::ReadOnlySrv,
_ => return None,
})
},
"test_shader",
)
.unwrap();
}
#[test]
fn srv_where_uav_expected_fails() {
let expectations = vec![Some(BindlessSlotKind::StorageUav)];
let err = validate_bindless_slot_kinds(
&[5],
&expectations,
|_| Some(BindlessSlotKind::ReadOnlySrv),
"game_of_life_render",
)
.unwrap_err()
.to_string();
assert!(err.contains("SRV/UAV mismatch"), "{err}");
assert!(err.contains("slot 0"), "{err}");
assert!(err.contains("storage UAV"), "{err}");
assert!(err.contains("read-only SRV"), "{err}");
}
}
#[cfg(test)]
mod binding_stride_validation_tests {
use super::validate_binding_strides;
#[test]
fn matching_strides_pass() {
let actual = vec![Some(4), Some(16)];
let expected = vec![Some(4), Some(16)];
validate_binding_strides(&actual, &expected, "test").unwrap();
}
#[test]
fn none_expected_skipped() {
let actual = vec![Some(4), Some(8)];
let expected: Vec<Option<u32>> = vec![None, None];
validate_binding_strides(&actual, &expected, "test").unwrap();
}
#[test]
fn none_actual_skipped() {
let actual: Vec<Option<u32>> = vec![None, None];
let expected = vec![Some(4), Some(16)];
validate_binding_strides(&actual, &expected, "test").unwrap();
}
#[test]
fn empty_expected_passes() {
let actual = vec![Some(4), Some(8)];
validate_binding_strides(&actual, &[], "test").unwrap();
}
#[test]
fn stride_mismatch_detected() {
let actual = vec![Some(4)];
let expected = vec![Some(16)];
let err = validate_binding_strides(&actual, &expected, "my_shader")
.unwrap_err()
.to_string();
assert!(err.contains("slot 0"), "should name slot: {err}");
assert!(err.contains("16"), "should mention expected stride: {err}");
assert!(err.contains("4"), "should mention actual stride: {err}");
}
#[test]
fn multiple_stride_mismatches_reported() {
let actual = vec![Some(4), Some(8)];
let expected = vec![Some(16), Some(32)];
let err = validate_binding_strides(&actual, &expected, "cs")
.unwrap_err()
.to_string();
assert!(err.contains("slot 0"), "should report slot 0: {err}");
assert!(err.contains("slot 1"), "should report slot 1: {err}");
}
#[test]
fn hint_message_mentions_both_scattered_and_broadcast() {
let actual = vec![Some(4)];
let expected = vec![Some(16)];
let err = validate_binding_strides(&actual, &expected, "cs")
.unwrap_err()
.to_string();
assert!(
err.to_lowercase().contains("scattered") || err.to_lowercase().contains("broadcast"),
"hint must mention both buffer kinds: {err}"
);
}
#[test]
fn missing_index_for_required_slot_is_an_error() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![42u32]; let categories = vec![
Some(ResourceCategory::Scattered), Some(ResourceCategory::Scattered), ];
let expected = vec![Some(16u32), Some(16u32)];
let err = validate_raw_binding_strides(
&indices,
&categories,
&expected,
|_idx, _cat| Some(16),
"my_compute_shader",
)
.unwrap_err()
.to_string();
assert!(err.contains("slot"), "error must name missing slot: {err}");
assert!(err.contains("my_compute_shader"), "error must name the shader: {err}");
assert!(err.contains('1'), "error must mention slot index 1: {err}");
}
#[test]
fn exact_index_count_passes() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![0u32, 1u32];
let categories = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Broadcast)];
let expected = vec![Some(16u32), Some(4u32)];
validate_raw_binding_strides(
&indices,
&categories,
&expected,
|_idx, cat| match cat {
ResourceCategory::Scattered => Some(16),
ResourceCategory::Broadcast => Some(4),
_ => None,
},
"ok_shader",
)
.unwrap();
}
#[test]
fn multi_binding_second_slot_mismatch() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![0u32, 1u32];
let categories = vec![Some(ResourceCategory::Broadcast), Some(ResourceCategory::Scattered)];
let expected = vec![Some(16u32), Some(16u32)];
let err = validate_raw_binding_strides(
&indices,
&categories,
&expected,
|idx, cat| match (idx, cat) {
(0, ResourceCategory::Broadcast) => Some(16),
(1, ResourceCategory::Scattered) => Some(4),
_ => None,
},
"struct_shader",
)
.unwrap_err()
.to_string();
assert!(err.contains("slot 1"), "error must identify slot 1: {err}");
assert!(err.contains("stride"), "error must mention stride: {err}");
}
#[test]
fn multi_binding_all_correct_passes() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![0u32, 1u32];
let categories = vec![Some(ResourceCategory::Broadcast), Some(ResourceCategory::Scattered)];
let expected = vec![Some(16u32), Some(16u32)];
validate_raw_binding_strides(&indices, &categories, &expected, |_idx, _cat| Some(16), "struct_shader").unwrap();
}
#[test]
fn broadcast_single_float_natural_stride_passes() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![0u32, 1u32];
let categories = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Broadcast)];
let expected = vec![Some(4u32), Some(4u32)];
validate_raw_binding_strides(
&indices,
&categories,
&expected,
|idx, cat| match (idx, cat) {
(0, ResourceCategory::Scattered) => Some(4),
(1, ResourceCategory::Broadcast) => Some(4),
_ => None,
},
"sim_params_shader",
)
.unwrap();
}
#[test]
fn broadcast_single_float_cbuffer_stride_fails() {
use super::validate_raw_binding_strides;
use crate::types::ResourceCategory;
let indices = vec![0u32, 1u32];
let categories = vec![Some(ResourceCategory::Scattered), Some(ResourceCategory::Broadcast)];
let expected = vec![Some(4u32), Some(4u32)];
let err = validate_raw_binding_strides(
&indices,
&categories,
&expected,
|idx, cat| match (idx, cat) {
(0, ResourceCategory::Scattered) => Some(4),
(1, ResourceCategory::Broadcast) => Some(16),
_ => None,
},
"sim_params_shader",
)
.unwrap_err()
.to_string();
assert!(err.contains("slot 1"), "params is slot 1: {err}");
assert!(err.contains("stride"), "error must mention stride: {err}");
}
#[test]
fn validate_render_pass_bind_resources_catches_mismatch() {
use super::validate_render_pass_bind_resources;
use crate::backend::{BufferHandle, PipelineHandle, RenderCommand};
let pipeline: PipelineHandle = 1;
let buf: BufferHandle = 1;
let commands = vec![
RenderCommand::SetPipeline(pipeline),
RenderCommand::BindResources { buffers: vec![buf] },
];
let err = validate_render_pass_bind_resources(
&commands,
|h| {
if h == 1 {
Some((vec![Some(4)], "test_shader".to_string()))
} else {
None
}
},
|h| {
if h == 1 {
Some(16)
} else {
None
}
},
)
.expect_err("mismatched render bind must fail");
assert!(err.to_string().contains("slot 0"));
}
}