use ash::vk;
use concinnity_core::render::uniforms::MAX_PROBES;
pub(in crate::vulkan) type Binding = (u32, vk::DescriptorType, vk::ShaderStageFlags);
pub(in crate::vulkan) fn global_set() -> [Binding; 8] {
use vk::DescriptorType as T;
use vk::ShaderStageFlags as S;
[
(0, T::UNIFORM_BUFFER, S::VERTEX | S::FRAGMENT),
(1, T::UNIFORM_BUFFER, S::FRAGMENT),
(2, T::UNIFORM_BUFFER, S::VERTEX | S::FRAGMENT),
(3, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(4, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(5, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(6, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(7, T::UNIFORM_BUFFER, S::FRAGMENT),
]
}
pub(in crate::vulkan) const PROBE_CUBE_ARRAY_BINDING: u32 = 8;
pub(in crate::vulkan) const LOCAL_LIGHT_SSBO_BINDING: u32 = 9;
pub(in crate::vulkan) const CLUSTER_PARAMS_UBO_BINDING: u32 = 10;
pub(in crate::vulkan) const CLUSTER_LIGHT_LIST_SSBO_BINDING: u32 = 11;
pub(in crate::vulkan) const SPOT_SHADOW_MAP_BINDING: u32 = 12;
pub(in crate::vulkan) const SPOT_SHADOW_DATA_SSBO_BINDING: u32 = 13;
pub(in crate::vulkan) const AREA_LIGHT_SSBO_BINDING: u32 = 14;
pub(in crate::vulkan) const LTC_MATRIX_BINDING: u32 = 15;
pub(in crate::vulkan) const LTC_MAGNITUDE_BINDING: u32 = 16;
const INLINE_GLOBAL_SAMPLERS: [u32; 3] = [
SPOT_SHADOW_MAP_BINDING,
LTC_MATRIX_BINDING,
LTC_MAGNITUDE_BINDING,
];
fn count_fragment_samplers(bindings: &[Binding]) -> u32 {
bindings
.iter()
.filter(|&&(_, ty, stage)| {
ty == vk::DescriptorType::COMBINED_IMAGE_SAMPLER
&& stage.contains(vk::ShaderStageFlags::FRAGMENT)
})
.count() as u32
}
fn global_fragment_samplers() -> u32 {
count_fragment_samplers(&global_set()) + INLINE_GLOBAL_SAMPLERS.len() as u32
}
fn object_fragment_samplers() -> u32 {
count_fragment_samplers(&object_set())
}
const TRANSPARENT_PASS_SAMPLERS: u32 = 3;
const REFLECTION_RESOLVE_SAMPLERS: u32 = 4;
fn widest_pass_samplers() -> u32 {
object_fragment_samplers()
.max(TRANSPARENT_PASS_SAMPLERS)
.max(REFLECTION_RESOLVE_SAMPLERS)
}
fn fixed_fragment_samplers() -> u32 {
global_fragment_samplers() + object_fragment_samplers()
}
pub(in crate::vulkan) fn probe_cube_array_count(
max_per_stage_samplers: u32,
global_update_after_bind: bool,
) -> u32 {
if global_update_after_bind {
return MAX_PROBES as u32;
}
let headroom = max_per_stage_samplers.saturating_sub(fixed_fragment_samplers());
headroom.clamp(1, MAX_PROBES as u32)
}
fn global_plain_samplers(probe_cube_count: u32, global_update_after_bind: bool) -> u32 {
if global_update_after_bind {
0
} else {
global_fragment_samplers() + probe_cube_count
}
}
pub(in crate::vulkan) fn bindless_pool_needs_update_after_bind(
max_per_stage_samplers: u32,
probe_cube_count: u32,
pool_size: u32,
global_update_after_bind: bool,
) -> bool {
let declared = global_plain_samplers(probe_cube_count, global_update_after_bind) + pool_size;
declared > max_per_stage_samplers
}
pub(in crate::vulkan) fn sampler_budget_is_constrained(max_per_stage_samplers: u32) -> bool {
global_fragment_samplers() + MAX_PROBES as u32 + widest_pass_samplers() > max_per_stage_samplers
}
pub(in crate::vulkan) fn object_set() -> [Binding; 2] {
use vk::DescriptorType as T;
use vk::ShaderStageFlags as S;
[
(0, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(1, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
]
}
pub(in crate::vulkan) fn shadow_global_set() -> [Binding; 1] {
[(
0,
vk::DescriptorType::UNIFORM_BUFFER,
vk::ShaderStageFlags::VERTEX,
)]
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_gap_free_and_unique(bindings: &[Binding]) {
let mut idx: Vec<u32> = bindings.iter().map(|b| b.0).collect();
idx.sort_unstable();
for (expected, &got) in idx.iter().enumerate() {
assert_eq!(
got, expected as u32,
"descriptor bindings must be 0..n gap-free and unique, got {idx:?}"
);
}
}
#[test]
fn geometry_path_sets_are_gap_free() {
assert_gap_free_and_unique(&global_set());
assert_gap_free_and_unique(&object_set());
assert_gap_free_and_unique(&shadow_global_set());
}
#[test]
fn global_set_contract_is_locked() {
use vk::DescriptorType as T;
use vk::ShaderStageFlags as S;
assert_eq!(
global_set(),
[
(0, T::UNIFORM_BUFFER, S::VERTEX | S::FRAGMENT),
(1, T::UNIFORM_BUFFER, S::FRAGMENT),
(2, T::UNIFORM_BUFFER, S::VERTEX | S::FRAGMENT),
(3, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(4, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(5, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(6, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(7, T::UNIFORM_BUFFER, S::FRAGMENT),
]
);
}
#[test]
fn probe_cube_array_binding_follows_global_set() {
assert_eq!(PROBE_CUBE_ARRAY_BINDING, global_set().len() as u32);
}
#[test]
fn local_light_ssbo_binding_follows_probe_cube_array() {
assert_eq!(LOCAL_LIGHT_SSBO_BINDING, PROBE_CUBE_ARRAY_BINDING + 1);
}
#[test]
fn cluster_bindings_follow_local_light_ssbo() {
assert_eq!(CLUSTER_PARAMS_UBO_BINDING, LOCAL_LIGHT_SSBO_BINDING + 1);
assert_eq!(
CLUSTER_LIGHT_LIST_SSBO_BINDING,
CLUSTER_PARAMS_UBO_BINDING + 1
);
}
#[test]
fn spot_shadow_bindings_follow_cluster_light_list() {
assert_eq!(SPOT_SHADOW_MAP_BINDING, CLUSTER_LIGHT_LIST_SSBO_BINDING + 1);
assert_eq!(SPOT_SHADOW_DATA_SSBO_BINDING, SPOT_SHADOW_MAP_BINDING + 1);
}
#[test]
fn area_light_bindings_follow_spot_shadow() {
assert_eq!(AREA_LIGHT_SSBO_BINDING, SPOT_SHADOW_DATA_SSBO_BINDING + 1);
assert_eq!(LTC_MATRIX_BINDING, AREA_LIGHT_SSBO_BINDING + 1);
assert_eq!(LTC_MAGNITUDE_BINDING, LTC_MATRIX_BINDING + 1);
}
#[test]
fn fixed_fragment_sampler_count_is_nine() {
assert_eq!(fixed_fragment_samplers(), 9);
assert_eq!(global_fragment_samplers(), 7);
assert_eq!(object_fragment_samplers(), 2);
}
#[test]
fn widest_pass_is_the_reflection_resolve() {
assert_eq!(object_fragment_samplers(), 2);
assert_eq!(TRANSPARENT_PASS_SAMPLERS, 3);
assert_eq!(REFLECTION_RESOLVE_SAMPLERS, 4);
assert_eq!(widest_pass_samplers(), REFLECTION_RESOLVE_SAMPLERS);
}
fn layout_plain_samplers(
probe_cube_count: u32,
global_update_after_bind: bool,
pass_samplers: u32,
) -> u32 {
global_plain_samplers(probe_cube_count, global_update_after_bind) + pass_samplers
}
#[test]
fn plain_global_set_overflows_transparent_and_ssr_on_moltenvk() {
let probes = probe_cube_array_count(16, false);
assert_eq!(probes, 7);
assert_eq!(
layout_plain_samplers(probes, false, object_fragment_samplers()),
16
);
assert_eq!(
layout_plain_samplers(probes, false, TRANSPARENT_PASS_SAMPLERS),
17
);
assert_eq!(
layout_plain_samplers(probes, false, REFLECTION_RESOLVE_SAMPLERS),
18
);
}
#[test]
fn update_after_bind_global_set_clears_every_layout_on_moltenvk() {
let probes = probe_cube_array_count(16, true);
assert_eq!(probes, MAX_PROBES as u32);
for pass in [
object_fragment_samplers(),
TRANSPARENT_PASS_SAMPLERS,
REFLECTION_RESOLVE_SAMPLERS,
] {
assert_eq!(layout_plain_samplers(probes, true, pass), pass);
assert!(layout_plain_samplers(probes, true, pass) <= 16);
}
}
#[test]
fn probe_cube_array_count_is_max_probes_on_desktop_drivers() {
for limit in [1_048_576, 500_000, 1_048_575] {
assert!(!sampler_budget_is_constrained(limit));
assert_eq!(probe_cube_array_count(limit, false), MAX_PROBES as u32);
}
}
#[test]
fn constrained_threshold_is_the_global_set_plus_the_widest_pass() {
let threshold = global_fragment_samplers() + MAX_PROBES as u32 + widest_pass_samplers();
assert_eq!(threshold, 19);
assert!(!sampler_budget_is_constrained(threshold));
assert!(sampler_budget_is_constrained(threshold - 1));
assert_eq!(probe_cube_array_count(threshold, false), MAX_PROBES as u32);
}
#[test]
fn probe_cube_array_count_fits_moltenvk_limit_without_update_after_bind() {
let count = probe_cube_array_count(16, false);
assert_eq!(count, 7);
assert_eq!(fixed_fragment_samplers() + count, 16);
}
#[test]
fn probe_cube_array_count_never_zero_or_over_ceiling() {
let fixed = fixed_fragment_samplers();
for limit in 0..=64u32 {
let count = probe_cube_array_count(limit, false);
assert!(count >= 1, "limit {limit} produced a zero-length array");
assert!(count <= MAX_PROBES as u32, "limit {limit} exceeded ceiling");
if limit > fixed {
assert!(
fixed + count <= limit,
"limit {limit} produced {count}, overrunning the budget"
);
}
assert_eq!(probe_cube_array_count(limit, true), MAX_PROBES as u32);
}
}
#[test]
fn bindless_pool_needs_update_after_bind_past_moltenvk_headroom() {
let probes = probe_cube_array_count(16, false);
assert!(!bindless_pool_needs_update_after_bind(16, probes, 2, false));
assert!(bindless_pool_needs_update_after_bind(16, probes, 3, false));
}
#[test]
fn bindless_pool_budgets_against_the_full_limit_under_global_update_after_bind() {
let probes = probe_cube_array_count(16, true);
assert!(!bindless_pool_needs_update_after_bind(16, probes, 16, true));
assert!(bindless_pool_needs_update_after_bind(16, probes, 17, true));
}
#[test]
fn bindless_pool_stays_plain_on_desktop_drivers() {
let probes = probe_cube_array_count(1_048_576, false);
for pool_size in [2, 64, 4096, 65_536] {
assert!(!bindless_pool_needs_update_after_bind(
1_048_576, probes, pool_size, false
));
}
}
#[test]
fn constrained_budget_covers_every_layout_that_can_overflow() {
for limit in 0..=64u32 {
let probes = probe_cube_array_count(limit, false);
let worst = layout_plain_samplers(probes, false, widest_pass_samplers());
if worst > limit || bindless_pool_needs_update_after_bind(limit, probes, 2, false) {
assert!(
sampler_budget_is_constrained(limit),
"limit {limit} can overflow but reads unconstrained"
);
}
}
assert!(sampler_budget_is_constrained(16));
assert!(!sampler_budget_is_constrained(1_048_576));
}
#[test]
fn object_and_shadow_sets_contract_is_locked() {
use vk::DescriptorType as T;
use vk::ShaderStageFlags as S;
assert_eq!(
object_set(),
[
(0, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
(1, T::COMBINED_IMAGE_SAMPLER, S::FRAGMENT),
]
);
assert_eq!(shadow_global_set(), [(0, T::UNIFORM_BUFFER, S::VERTEX)]);
}
}