use concinnity_core::render::error::{RenderError, RenderResult};
use windows::Win32::Graphics::Direct3D::D3D_FEATURE_LEVEL_11_0;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::*;
use crate::directx::error::map_hresult;
pub(super) struct Selection {
pub adapter: IDXGIAdapter1,
pub device: ID3D12Device,
}
pub(super) fn select(factory: &IDXGIFactory4) -> RenderResult<Selection> {
let mut rejections: Vec<String> = Vec::new();
let mut i = 0u32;
while let Ok(adapter) = unsafe { factory.EnumAdapters1(i) } {
i += 1;
let Ok(desc) = (unsafe { adapter.GetDesc1() }) else {
continue;
};
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0 {
continue;
}
match consider(&adapter) {
Ok(selection) => return Ok(selection),
Err(reason) => rejections.push(reason.to_string()),
}
}
match unsafe { factory.EnumWarpAdapter::<IDXGIAdapter1>() } {
Ok(warp) => match consider(&warp) {
Ok(selection) => {
tracing::warn!(
"d3d12 adapter: falling back to the software adapter, which renders \
every pass on the CPU and is far slower than any GPU. No hardware \
adapter could run the renderer: {}",
rejections.join("; ")
);
return Ok(selection);
}
Err(reason) => rejections.push(reason.to_string()),
},
Err(e) => rejections.push(format!("the software adapter is unavailable: {e}")),
}
Err(RenderError::Other(no_usable_adapter_message(&rejections)))
}
fn consider(adapter: &IDXGIAdapter1) -> RenderResult<Selection> {
let mut device_opt: Option<ID3D12Device> = None;
unsafe { D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_11_0, &mut device_opt) }.map_err(
|e| {
map_hresult(
e.code(),
&format!("{}: D3D12CreateDevice", adapter_name(adapter)),
)
},
)?;
let device = device_opt.ok_or_else(|| {
RenderError::Other(format!(
"{}: D3D12CreateDevice returned None",
adapter_name(adapter)
))
})?;
if let Some(refusal) =
binding_tier_refusal(resource_binding_tier(&device), &adapter_name(adapter))
{
return Err(RenderError::Other(refusal));
}
Ok(Selection {
adapter: adapter.clone(),
device,
})
}
pub(super) fn resource_binding_tier(device: &ID3D12Device) -> D3D12_RESOURCE_BINDING_TIER {
let mut options = D3D12_FEATURE_DATA_D3D12_OPTIONS::default();
let ok = unsafe {
device.CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS,
&mut options as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<D3D12_FEATURE_DATA_D3D12_OPTIONS>() as u32,
)
};
if ok.is_ok() {
options.ResourceBindingTier
} else {
D3D12_RESOURCE_BINDING_TIER_1
}
}
pub(super) fn binding_tier_refusal(
tier: D3D12_RESOURCE_BINDING_TIER,
adapter: &str,
) -> Option<String> {
if tier.0 >= D3D12_RESOURCE_BINDING_TIER_3.0 {
return None;
}
Some(format!(
"{adapter} reports D3D12 resource binding tier {}; the renderer needs tier 3, \
because it binds its textures through an unbounded descriptor range",
tier.0
))
}
fn no_usable_adapter_message(rejections: &[String]) -> String {
compose_refusal(rejections, cfg!(agility_sdk_configured), || {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|d| d.join("D3D12").display().to_string()))
})
}
fn compose_refusal(
rejections: &[String],
agility_bundled: bool,
agility_dir: impl Fn() -> Option<String>,
) -> String {
let mut message = if rejections.is_empty() {
"no D3D12 adapter found".to_string()
} else {
format!("no usable D3D12 adapter: {}", rejections.join("; "))
};
if agility_bundled {
message.push_str(&format!(
". This binary was built with CN_ENABLE_AGILITY_SDK=1, so it bundles \
Microsoft's Agility SDK and needs D3D12Core.dll in {}; without it \
D3D12 fails to start and every adapter reports unsupported. Copy \
that directory next to the executable, or rebuild without the \
opt-in to use the OS D3D12 runtime",
agility_dir()
.unwrap_or_else(|| "a `D3D12` directory beside the executable".to_string())
));
}
message
}
pub(super) fn adapter_name(adapter: &IDXGIAdapter1) -> String {
let Ok(desc) = (unsafe { adapter.GetDesc1() }) else {
return "this adapter".to_string();
};
let end = desc
.Description
.iter()
.position(|&c| c == 0)
.unwrap_or(desc.Description.len());
String::from_utf16_lossy(&desc.Description[..end])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tier_three_is_what_the_texture_pool_needs() {
assert_eq!(
binding_tier_refusal(D3D12_RESOURCE_BINDING_TIER_3, "GPU"),
None
);
}
#[test]
fn a_lower_tier_is_refused_by_name_and_number() {
for tier in [D3D12_RESOURCE_BINDING_TIER_1, D3D12_RESOURCE_BINDING_TIER_2] {
let refusal = binding_tier_refusal(tier, "Parallels Display Adapter")
.expect("a tier below 3 cannot bind the texture pool");
assert!(refusal.contains("Parallels Display Adapter"), "{refusal}");
assert!(refusal.contains(&tier.0.to_string()), "{refusal}");
}
}
#[test]
fn every_rejection_reaches_the_message() {
let message = compose_refusal(
&[
"one is tier 1".to_string(),
"the software adapter is unavailable".to_string(),
],
false,
|| None,
);
assert!(message.contains("one is tier 1"), "{message}");
assert!(
message.contains("the software adapter is unavailable"),
"{message}"
);
}
#[test]
fn an_enumeration_that_found_nothing_says_so() {
let message = compose_refusal(&[], false, || None);
assert!(message.contains("no D3D12 adapter found"), "{message}");
}
#[test]
fn a_bundled_agility_sdk_names_the_directory_it_needs() {
let message = compose_refusal(&["D3D12CreateDevice failed".to_string()], true, || {
Some("C:\\app\\D3D12".to_string())
});
assert!(message.contains("C:\\app\\D3D12"), "{message}");
assert!(message.contains("D3D12Core.dll"), "{message}");
}
#[test]
fn an_unlocatable_agility_directory_still_names_what_is_missing() {
let message = compose_refusal(&[], true, || None);
assert!(message.contains("D3D12Core.dll"), "{message}");
assert!(message.contains("beside the executable"), "{message}");
}
}