rt: make runtimes thread safe and document thread safety guarantees

This commit is contained in:
chyyran 2023-02-10 01:29:49 -05:00
parent 7f17569b58
commit 512a4c0050
21 changed files with 142 additions and 88 deletions

1
Cargo.lock generated
View file

@ -940,6 +940,7 @@ dependencies = [
"librashader-reflect",
"librashader-runtime",
"librashader-spirv-cross",
"parking_lot",
"rayon",
"rustc-hash",
"thiserror",

View file

@ -10,23 +10,21 @@ librashader (*/ˈli:brəʃeɪdɚ/*) is a preprocessor, compiler, and runtime for
![Nightly rust](https://img.shields.io/badge/rust-nightly-orange.svg)
## Supported Render APIs
librashader supports OpenGL 3, OpenGL 4.6, Vulkan, Direct3D 11, and Direct3D 12. Older versions
of Direct3D and OpenGL, as well as Metal, are not supported (but pull-requests are welcome).
librashader supports OpenGL 3, OpenGL 4.6, Vulkan, Direct3D 11, and Direct3D 12. Metal and WebGPU
are not currently supported (but pull-requests are welcome). librashader does not support legacy render
APIs such as older versions of OpenGL, or legacy versions of Direct3D.
| **API** | **Status** | **`librashader` feature** |
|-------------|------------|---------------------------|
| OpenGL 3.3+ | ✔ | `gl` |
| OpenGL 4.6 | ✔ | `gl` |
| Vulkan | ✔ | `vk` |
| Direct3D 11 | ✔ | `d3d11` |
| Direct3D 12 | ✔ | `d3d12` |
| OpenGL 2 | ❌ | |
| Direct3D 9 | ❌ | |
| Direct3D 10 | ❌ | |
| Metal | ❌ | |
|-------------|------------|--------------------------|
| OpenGL 3.3+ | ✔ | `gl` |
| OpenGL 4.6 | ✔ | `gl` |
| Vulkan | ✔ | `vk` |
| Direct3D 11 | ✔ | `d3d11` |
| Direct3D 12 | ✔ | `d3d12` |
| Metal | ❌ | |
| WebGPU | ❌ | |
✔ = Render API is supported — 🚧 = Support is in progress — ❌ Render API is not supported
✔ = Render API is supported — ❌ Render API is not supported
## Usage
librashader provides both a Rust API under the `librashader` crate, and a C API. Both APIs are first-class and fully supported.
@ -68,9 +66,9 @@ is important to ensure that updates to librashader do not break existing consume
As of `0.1.0-rc.3`, the C ABI should be mostly stable. We reserve the right to make breaking changes before a numbered
release without following semantic versioning.
Linking against `librashader.h` directly is possible, but is not officially supported. You will need to ensure linkage
parameters are correct in order to successfully link with `librashader.lib` or `librashader.a`. The [corrosion](https://github.com/corrosion-rs/)
CMake package is highly recommended.
Linking statically against `librashader.h` is possible, but is not officially supported. You will need to ensure
linkage parameters are correct in order to successfully link with `librashader.lib` or `librashader.a`.
The [corrosion](https://github.com/corrosion-rs/) CMake package is highly recommended.
### Examples
@ -153,6 +151,13 @@ static GL_DEFAULT_MVP: &[f32; 16] = &[
];
```
### Thread safety
In general, it is **safe** to create a filter chain instance from a different thread, but drawing filter passes **must be
externally synchronized**. The exceptions to filter chain creation are in OpenGL, where creating the filter chain instance
is safe **if and only if** the thread local OpenGL context is initialized to the same context as the drawing thread, and
in Direct3D 11, where filter chain creation is thread-unsafe if the `ID3D11Device` was created with
`D3D11_CREATE_DEVICE_SINGLETHREADED`.
### Writing a librashader Runtime
If you wish to contribute a runtime implementation not already available, see the [librashader-runtime](https://docs.rs/librashader-runtime/latest/librashader_runtime/)

View file

@ -831,6 +831,10 @@ libra_error_t libra_gl_filter_chain_create(libra_shader_preset_t *preset,
/// values for the model view projection matrix.
/// - `opt` may be null, or if it is not null, must be an aligned pointer to a valid `frame_gl_opt_t`
/// struct.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function. The thread `libra_gl_filter_chain_frame` is called from
/// must have its thread-local OpenGL context initialized with the same context used to create
/// the filter chain.
libra_error_t libra_gl_filter_chain_frame(libra_gl_filter_chain_t *chain,
size_t frame_count,
struct libra_source_image_gl_t image,
@ -925,6 +929,8 @@ libra_error_t libra_vk_filter_chain_create(struct libra_device_vk_t vulkan,
/// values for the model view projection matrix.
/// - `opt` may be null, or if it is not null, must be an aligned pointer to a valid `frame_vk_opt_t`
/// struct.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
libra_error_t libra_vk_filter_chain_frame(libra_vk_filter_chain_t *chain,
VkCommandBuffer command_buffer,
size_t frame_count,
@ -1015,6 +1021,8 @@ libra_error_t libra_d3d11_filter_chain_create(libra_shader_preset_t *preset,
/// struct.
/// - `out` must not be null.
/// - `image.handle` must not be null.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
libra_error_t libra_d3d11_filter_chain_frame(libra_d3d11_filter_chain_t *chain,
size_t frame_count,
struct libra_source_image_d3d11_t image,
@ -1106,6 +1114,8 @@ libra_error_t libra_d3d12_filter_chain_create(libra_shader_preset_t *preset,
/// - `out` must be a descriptor handle to a render target view.
/// - `image.resource` must not be null.
/// - `command_list` must not be null.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
libra_error_t libra_d3d12_filter_chain_frame(libra_d3d12_filter_chain_t *chain,
const ID3D12GraphicsCommandList * command_list,
size_t frame_count,

View file

@ -51,6 +51,18 @@
//! There is a case to be made for skipping error checking for `*_filter_chain_frame` due to performance reasons,
//! but only if you are certain that the safety invariants are upheld on each call. Failure to check for errors
//! may result in **undefined behaviour** stemming from failure to uphold safety invariants.
//!
//! ## Thread safety
//!
//! In general, it is **safe** to create a filter chain instance from a different thread, but drawing filter passes must be
//! synchronized externally. The exception to filter chain creation are in OpenGL, where creating the filter chain instance
//! is safe **if and only if** the thread local OpenGL context is initialized to the same context as the drawing thread, and
//! in Direct3D 11, where filter chain creation is unsafe if the `ID3D11Device` was created with
//! `D3D11_CREATE_DEVICE_SINGLETHREADED`.
//!
//! You must ensure that only thread has access to a created filter pass **before** you call `*_frame`. `*_frame` may only be
//! called from one thread at a time.
#![allow(non_camel_case_types)]
#![feature(try_blocks)]
#![feature(pointer_is_aligned)]
@ -73,3 +85,6 @@ pub type LIBRASHADER_API_VERSION = usize;
/// The current version of the librashader API/ABI.
/// Pass this into `version` for config structs.
pub const LIBRASHADER_CURRENT_VERSION: LIBRASHADER_API_VERSION = 0;
#[allow(dead_code)]
const fn assert_thread_safe<T: Send + Sync>() { }

View file

@ -7,6 +7,8 @@ use std::ffi::{c_char, CStr, CString};
use std::mem::MaybeUninit;
use std::ptr::NonNull;
const _: () = crate::assert_thread_safe::<ShaderPreset>();
/// A list of preset parameters.
#[repr(C)]
pub struct libra_preset_param_list_t {

View file

@ -13,8 +13,8 @@ use windows::Win32::Graphics::Direct3D11::{
ID3D11Device, ID3D11RenderTargetView, ID3D11ShaderResourceView,
};
pub use librashader::runtime::d3d11::capi::options::FilterChainOptionsD3D11;
pub use librashader::runtime::d3d11::capi::options::FrameOptionsD3D11;
use librashader::runtime::d3d11::capi::options::FilterChainOptionsD3D11;
use librashader::runtime::d3d11::capi::options::FrameOptionsD3D11;
use librashader::runtime::{FilterChainParameters, Size, Viewport};
use crate::LIBRASHADER_API_VERSION;
@ -142,6 +142,8 @@ extern_fn! {
/// struct.
/// - `out` must not be null.
/// - `image.handle` must not be null.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
fn libra_d3d11_filter_chain_frame(
chain: *mut libra_d3d11_filter_chain_t,
frame_count: usize,

View file

@ -2,3 +2,4 @@
mod filter_chain;
pub use filter_chain::*;
const _: () = crate::assert_thread_safe::<librashader::runtime::d3d11::FilterChain>();

View file

@ -13,8 +13,8 @@ use windows::Win32::Graphics::Direct3D12::{
};
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT;
pub use librashader::runtime::d3d12::capi::options::FilterChainOptionsD3D12;
pub use librashader::runtime::d3d12::capi::options::FrameOptionsD3D12;
use librashader::runtime::d3d12::capi::options::FilterChainOptionsD3D12;
use librashader::runtime::d3d12::capi::options::FrameOptionsD3D12;
use librashader::runtime::d3d12::{D3D12InputImage, D3D12OutputView};
use librashader::runtime::{FilterChainParameters, Size, Viewport};
@ -159,6 +159,8 @@ extern_fn! {
/// - `out` must be a descriptor handle to a render target view.
/// - `image.resource` must not be null.
/// - `command_list` must not be null.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
fn libra_d3d12_filter_chain_frame(
chain: *mut libra_d3d12_filter_chain_t,
command_list: ManuallyDrop<ID3D12GraphicsCommandList>,

View file

@ -2,3 +2,4 @@
mod filter_chain;
pub use filter_chain::*;
const _: () = crate::assert_thread_safe::<librashader::runtime::d3d12::FilterChain>();

View file

@ -10,8 +10,8 @@ use std::mem::MaybeUninit;
use std::ptr::NonNull;
use std::slice;
pub use librashader::runtime::gl::capi::options::FilterChainOptionsGL;
pub use librashader::runtime::gl::capi::options::FrameOptionsGL;
use librashader::runtime::gl::capi::options::FilterChainOptionsGL;
use librashader::runtime::gl::capi::options::FrameOptionsGL;
use librashader::runtime::FilterChainParameters;
use librashader::runtime::{Size, Viewport};
use crate::LIBRASHADER_API_VERSION;
@ -162,6 +162,10 @@ extern_fn! {
/// values for the model view projection matrix.
/// - `opt` may be null, or if it is not null, must be an aligned pointer to a valid `frame_gl_opt_t`
/// struct.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function. The thread `libra_gl_filter_chain_frame` is called from
/// must have its thread-local OpenGL context initialized with the same context used to create
/// the filter chain.
fn libra_gl_filter_chain_frame(
chain: *mut libra_gl_filter_chain_t,
frame_count: usize,

View file

@ -2,3 +2,4 @@
mod filter_chain;
pub use filter_chain::*;
const _: () = crate::assert_thread_safe::<librashader::runtime::gl::FilterChain>();

View file

@ -10,8 +10,8 @@ use std::mem::MaybeUninit;
use std::ptr::NonNull;
use std::slice;
pub use librashader::runtime::vk::capi::options::FilterChainOptionsVulkan;
pub use librashader::runtime::vk::capi::options::FrameOptionsVulkan;
use librashader::runtime::vk::capi::options::FilterChainOptionsVulkan;
use librashader::runtime::vk::capi::options::FrameOptionsVulkan;
use librashader::runtime::FilterChainParameters;
use librashader::runtime::{Size, Viewport};
@ -182,6 +182,8 @@ extern_fn! {
/// values for the model view projection matrix.
/// - `opt` may be null, or if it is not null, must be an aligned pointer to a valid `frame_vk_opt_t`
/// struct.
/// - You must ensure that only one thread has access to `chain` before you call this function. Only one
/// thread at a time may call this function.
fn libra_vk_filter_chain_frame(
chain: *mut libra_vk_filter_chain_t,
command_buffer: vk::CommandBuffer,

View file

@ -2,3 +2,4 @@
mod filter_chain;
pub use filter_chain::*;
const _: () = crate::assert_thread_safe::<librashader::runtime::vk::FilterChain>();

View file

@ -19,6 +19,7 @@ librashader-reflect = { path = "../librashader-reflect", version = "0.1.0-rc.2",
librashader-runtime = { path = "../librashader-runtime", version = "0.1.0-rc.2" }
thiserror = "1.0.37"
spirv_cross = { package = "librashader-spirv-cross", version = "0.23" }
parking_lot = "0.12.1"
rustc-hash = "1.1.0"
bytemuck = { version = "1.12.3", features = ["derive"] }

View file

@ -100,8 +100,10 @@ impl D3D12Buffer {
/// SAFETY: Creating the pointer should be safe in multithreaded contexts.
///
/// Mutation is guarded by DerefMut<Target=[u8]>
/// Mutation is guarded by DerefMut<Target=[u8]>, so exclusive access is guaranteed.
/// We do not ever leak the pointer to C.
unsafe impl Send for RawD3D12Buffer {}
unsafe impl Sync for RawD3D12Buffer {}
pub struct RawD3D12Buffer {
buffer: ManuallyDrop<D3D12Buffer>,
ptr: NonNull<c_void>,

View file

@ -1,10 +1,10 @@
use crate::error;
use bitvec::bitvec;
use bitvec::boxed::BitBox;
use std::cell::RefCell;
use parking_lot::RwLock;
use std::marker::PhantomData;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::Arc;
use crate::error::FilterChainError;
use windows::Win32::Graphics::Direct3D12::{
@ -98,12 +98,12 @@ impl const D3D12HeapType for SamplerWorkHeap {
}
}
pub type D3D12DescriptorHeapSlot<T> = Rc<D3D12DescriptorHeapSlotInner<T>>;
pub type D3D12DescriptorHeapSlot<T> = Arc<D3D12DescriptorHeapSlotInner<T>>;
pub struct D3D12DescriptorHeapSlotInner<T> {
cpu_handle: D3D12_CPU_DESCRIPTOR_HANDLE,
gpu_handle: Option<D3D12_GPU_DESCRIPTOR_HANDLE>,
heap: Rc<RefCell<D3D12DescriptorHeapInner>>,
heap: Arc<RwLock<D3D12DescriptorHeapInner>>,
slot: usize,
_pd: PhantomData<T>,
}
@ -117,7 +117,7 @@ impl<T> D3D12DescriptorHeapSlotInner<T> {
/// unsafe because type must match
pub unsafe fn copy_descriptor(&self, source: D3D12_CPU_DESCRIPTOR_HANDLE) {
unsafe {
let heap = self.heap.deref().borrow();
let heap = self.heap.deref().read();
heap.device
.CopyDescriptorsSimple(1, self.cpu_handle, source, heap.ty)
@ -142,7 +142,7 @@ impl<T: D3D12ShaderVisibleHeapType> AsRef<D3D12_GPU_DESCRIPTOR_HANDLE>
impl<T: D3D12ShaderVisibleHeapType> From<&D3D12DescriptorHeap<T>> for ID3D12DescriptorHeap {
fn from(value: &D3D12DescriptorHeap<T>) -> Self {
value.0.borrow().heap.clone()
value.0.read().heap.clone()
}
}
@ -159,7 +159,7 @@ struct D3D12DescriptorHeapInner {
map: BitBox,
}
pub struct D3D12DescriptorHeap<T>(Rc<RefCell<D3D12DescriptorHeapInner>>, PhantomData<T>);
pub struct D3D12DescriptorHeap<T>(Arc<RwLock<D3D12DescriptorHeapInner>>, PhantomData<T>);
impl<T: D3D12HeapType> D3D12DescriptorHeap<T> {
pub fn new(device: &ID3D12Device, size: usize) -> error::Result<D3D12DescriptorHeap<T>> {
@ -171,7 +171,7 @@ impl<T: D3D12HeapType> D3D12DescriptorHeap<T> {
impl<T> D3D12DescriptorHeap<T> {
/// Gets a cloned handle to the inner heap
pub fn handle(&self) -> ID3D12DescriptorHeap {
let inner = self.0.borrow();
let inner = self.0.read();
inner.heap.clone()
}
@ -190,7 +190,7 @@ impl<T> D3D12DescriptorHeap<T> {
};
Ok(D3D12DescriptorHeap(
Rc::new(RefCell::new(D3D12DescriptorHeapInner {
Arc::new(RwLock::new(D3D12DescriptorHeapInner {
device: device.clone(),
heap,
ty: desc.Type,
@ -224,12 +224,12 @@ impl<T> D3D12DescriptorHeap<T> {
) {
// has to be called right after creation.
assert_eq!(
Rc::strong_count(&self.0),
Arc::strong_count(&self.0),
1,
"D3D12DescriptorHeap::suballocate can only be callled immediately after creation."
);
let inner = Rc::try_unwrap(self.0)
let inner = Arc::try_unwrap(self.0)
.expect("[d3d12] undefined behaviour to suballocate a descriptor heap with live descriptors.")
.into_inner();
@ -301,11 +301,11 @@ impl<T> D3D12DescriptorHeap<T> {
heaps
.into_iter()
.map(|inner| {
D3D12DescriptorHeap(Rc::new(RefCell::new(inner)), PhantomData::default())
D3D12DescriptorHeap(Arc::new(RwLock::new(inner)), PhantomData::default())
})
.collect(),
reserved_heap.map(|inner| {
D3D12DescriptorHeap(Rc::new(RefCell::new(inner)), PhantomData::default())
D3D12DescriptorHeap(Arc::new(RwLock::new(inner)), PhantomData::default())
}),
inner.heap,
)
@ -314,7 +314,7 @@ impl<T> D3D12DescriptorHeap<T> {
pub fn alloc_slot(&mut self) -> error::Result<D3D12DescriptorHeapSlot<T>> {
let mut handle = D3D12_CPU_DESCRIPTOR_HANDLE { ptr: 0 };
let mut inner = self.0.borrow_mut();
let mut inner = self.0.write();
for i in inner.start..inner.num_descriptors {
if !inner.map[i] {
inner.map.set(i, true);
@ -327,10 +327,10 @@ impl<T> D3D12DescriptorHeap<T> {
ptr: (handle.ptr as u64 - inner.cpu_start.ptr as u64) + gpu_start.ptr,
});
return Ok(Rc::new(D3D12DescriptorHeapSlotInner {
return Ok(Arc::new(D3D12DescriptorHeapSlotInner {
cpu_handle: handle,
slot: i,
heap: Rc::clone(&self.0),
heap: Arc::clone(&self.0),
gpu_handle,
_pd: Default::default(),
}));
@ -352,7 +352,7 @@ impl<T> D3D12DescriptorHeap<T> {
impl<T> Drop for D3D12DescriptorHeapSlotInner<T> {
fn drop(&mut self) {
let mut inner = self.heap.borrow_mut();
let mut inner = self.heap.write();
inner.map.set(self.slot, false);
if inner.start > self.slot {
inner.start = self.slot

View file

@ -403,6 +403,8 @@ impl FilterChainD3D12 {
let filters: Vec<error::Result<_>> = passes.into_par_iter()
.zip(hlsl_passes)
.zip(work_heaps)
.zip(sampler_work_heaps)
.enumerate()
.map_init(
|| {
@ -411,8 +413,8 @@ impl FilterChainD3D12 {
let compiler: IDxcCompiler = unsafe { DxcCreateInstance(&CLSID_DxcCompiler)? };
Ok::<_, FilterChainError>((validator, library, compiler))
},
|dxc, (index, ((config, source, mut dxil),
(_, _, mut hlsl)))| {
|dxc, (index, ((((config, source, mut dxil),
(_, _, mut hlsl)), mut texture_heap), mut sampler_heap))| {
let Ok((validator, library, compiler)) = dxc else {
return Err(FilterChainError::Direct3DOperationError("Could not initialize DXC for thread"));
};
@ -472,48 +474,55 @@ impl FilterChainD3D12 {
let uniform_bindings = reflection.meta.create_binding_map(|param| param.offset());
Ok((reflection,
uniform_bindings,
uniform_storage,
graphics_pipeline,
config,
source))
}).collect();
let filters: error::Result<Vec<_>> = filters.into_iter().collect();
let filters = filters?;
// Need to take care of the heaps in a single thread because [;16] is not sized..?
let filters: Vec<error::Result<FilterPass>> = filters
.into_iter()
.zip(work_heaps)
.zip(sampler_work_heaps)
.map(
|(
(
(reflection, uniform_bindings, uniform_storage, pipeline, config, source),
mut texture_heap,
),
mut sampler_heap,
)| {
let texture_heap = texture_heap.alloc_range()?;
let sampler_heap = sampler_heap.alloc_range()?;
Ok(FilterPass {
reflection,
uniform_bindings,
uniform_storage,
pipeline,
pipeline: graphics_pipeline,
config,
texture_heap,
sampler_heap,
source,
})
},
)
.collect();
}).collect();
let filters: error::Result<Vec<_>> = filters.into_iter().collect();
let filters = filters?;
//
// // Need to take care of the heaps in a single thread because [;16] is not sized..?
// let filters: Vec<error::Result<FilterPass>> = filters
// .into_iter()
// .zip(work_heaps)
// .zip(sampler_work_heaps)
// .map(
// |(
// (
// (reflection, uniform_bindings, uniform_storage, pipeline, config, source),
// mut texture_heap,
// ),
// mut sampler_heap,
// )| {
// let texture_heap = texture_heap.alloc_range()?;
// let sampler_heap = sampler_heap.alloc_range()?;
// Ok(FilterPass {
// reflection,
// uniform_bindings,
// uniform_storage,
// pipeline,
// config,
// texture_heap,
// sampler_heap,
// source,
// })
// },
// )
// .collect();
// let filters: error::Result<Vec<_>> = filters.into_iter().collect();
// let filters = filters?;
// Panic SAFETY: mipmap_heap is always 1024 descriptors.
Ok((

View file

@ -261,7 +261,6 @@ impl<T: GLInterface> FilterChainImpl<T> {
filters.push(FilterPass {
reflection,
compiled: glsl,
program,
ubo_location,
ubo_ring,

View file

@ -1,6 +1,4 @@
use gl::types::{GLint, GLsizei, GLuint};
use librashader_reflect::back::cross::CrossGlslContext;
use librashader_reflect::back::ShaderCompilerOutput;
use librashader_reflect::reflect::ShaderReflection;
use librashader_common::{ImageFormat, Size, Viewport};
@ -33,7 +31,6 @@ impl UniformOffset {
pub struct FilterPass<T: GLInterface> {
pub reflection: ShaderReflection,
pub compiled: ShaderCompilerOutput<String, CrossGlslContext>,
pub program: GLuint,
pub ubo_location: UniformLocation<GLuint>,
pub ubo_ring: Option<T::UboRing>,

View file

@ -130,8 +130,10 @@ impl Drop for VulkanBuffer {
/// SAFETY: Creating the pointer should be safe in multithreaded contexts.
///
/// Mutation is guarded by DerefMut<Target=[u8]>
/// Mutation is guarded by DerefMut<Target=[u8]>, so exclusive access is guaranteed.
/// We do not ever leak the pointer to C.
unsafe impl Send for RawVulkanBuffer {}
unsafe impl Sync for RawVulkanBuffer {}
pub struct RawVulkanBuffer {
buffer: ManuallyDrop<VulkanBuffer>,
ptr: NonNull<c_void>,

View file

@ -26,16 +26,13 @@
//!
//! | **API** | **Status** | **`librashader` feature** |
//! |-------------|------------|---------------------------|
//! | OpenGL 3.3+ | ✔ | `gl` |
//! | OpenGL 4.6 | ✔ | `gl` |
//! | OpenGL 3.3+ | ✔ | `gl` |
//! | OpenGL 4.6 | ✔ | `gl` |
//! | Vulkan | ✔ | `vk` |
//! | Direct3D 11 | ✔ | `d3d11` |
//! | Direct3D 12 | ✔ | `d3d12` |
//! | OpenGL 2 | ❌ | |
//! | Direct3D 9 | ❌ | |
//! | Direct3D 10 | ❌ | |
//! | Metal | ❌ | |
//!
//! | Direct3D 11 | ✔ | `d3d11` |
//! | Direct3D 12 | ✔ | `d3d12` |
//! | Metal | ❌ | |
//! | WebGPU | ❌ | |
//! ## C API
//! For documentation on the librashader C API, see [librashader-capi](https://docs.rs/librashader-capi/latest/librashader_capi/),
//! or [`librashader.h`](https://github.com/SnowflakePowered/librashader/blob/master/include/librashader.h).