fix(render): preserve projected shadow depth bias in Vulkan pipelines

This commit is contained in:
Valentin Popov committed 2026-10-31 17:16:52 +04:00
1 parent 6c7740f133
commit 4a589f42fc
4 files changed
+157 -20

No files matched your search

@@ -2577,7 +2577,9 @@ mod tests {
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order, take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop, validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
}; };
use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState}; use fparkan_render::{
LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
};
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -3144,6 +3146,67 @@ mod tests {
); );
} }
#[test]
fn control_fx_queue_follows_opaque_and_sky_without_reactivating_empty_ranges() {
const SCREEN_GRADIENT: usize = 0;
const CONTROL_FX: usize = 1;
const TERRAIN: usize = 2;
const BUILDING: usize = 3;
const SHADOW: usize = 4;
const NEBULA: usize = 5;
const DOME: usize = 6;
const CLOUDS: usize = 7;
const EMPTY_CONTROL_FX: usize = 8;
const SUN: usize = 9;
const WEATHER: usize = 10;
let mut ranges = (0..11)
.map(|index| VulkanStaticDrawRange {
first_index: index * 3,
index_count: 3,
material_index: 0,
lightmap_index: u8::MAX,
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
ranges[EMPTY_CONTROL_FX].index_count = 0;
let mut world_queue = vec![None; ranges.len()];
world_queue[TERRAIN] = Some(false);
world_queue[BUILDING] = Some(false);
world_queue[CONTROL_FX] = Some(true);
world_queue[EMPTY_CONTROL_FX] = Some(true);
let mut sort_keys = vec![None; ranges.len()];
sort_keys[CONTROL_FX] = Some(20.0);
sort_keys[EMPTY_CONTROL_FX] = Some(30.0);
let order = sorted_world_draw_order(
ranges.len(),
&ranges,
&world_queue,
&sort_keys,
&[NEBULA, DOME, CLOUDS],
);
assert_eq!(
order,
vec![
SCREEN_GRADIENT,
TERRAIN,
BUILDING,
SHADOW,
NEBULA,
DOME,
CLOUDS,
CONTROL_FX,
EMPTY_CONTROL_FX,
SUN,
WEATHER,
]
);
assert_eq!(ranges[EMPTY_CONTROL_FX].index_count, 0);
}
#[test] #[test]
fn material_category_and_transparency_preserve_world_state_in_either_order() { fn material_category_and_transparency_preserve_world_state_in_either_order() {
let original = LegacyPipelineState { let original = LegacyPipelineState {
@@ -3151,6 +3214,7 @@ mod tests {
depth: LegacyDepthMode::TestWrite, depth: LegacyDepthMode::TestWrite,
cull: LegacyCullMode::FrontFace, cull: LegacyCullMode::FrontFace,
alpha_test: false, alpha_test: false,
depth_bias: LegacyDepthBias::ProjectedDecal,
}; };
let (base, category_first) = let (base, category_first) =
world_material_pipeline_states(original, 1, Some(true)).expect("category 1"); world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
@@ -3162,14 +3226,17 @@ mod tests {
assert!(base.alpha_test); assert!(base.alpha_test);
assert_eq!(base.depth, LegacyDepthMode::TestWrite); assert_eq!(base.depth, LegacyDepthMode::TestWrite);
assert_eq!(base.cull, LegacyCullMode::FrontFace); assert_eq!(base.cull, LegacyCullMode::FrontFace);
assert_eq!(base.depth_bias, LegacyDepthBias::ProjectedDecal);
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly); assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
assert_eq!(category_first.cull, LegacyCullMode::FrontFace); assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
assert_eq!(category_first.depth_bias, LegacyDepthBias::ProjectedDecal);
let fixed_state = LegacyPipelineState { let fixed_state = LegacyPipelineState {
blend: LegacyBlendMode::Additive, blend: LegacyBlendMode::Additive,
depth: LegacyDepthMode::Disabled, depth: LegacyDepthMode::Disabled,
cull: LegacyCullMode::Disabled, cull: LegacyCullMode::Disabled,
alpha_test: false, alpha_test: false,
depth_bias: LegacyDepthBias::Disabled,
}; };
assert_eq!( assert_eq!(
world_material_pipeline_states(fixed_state, 1, None), world_material_pipeline_states(fixed_state, 1, None),
@@ -756,8 +756,8 @@ impl VulkanStaticMesh {
material_index: 0, material_index: 0,
lightmap_index: u8::MAX, lightmap_index: u8::MAX,
batch_flags: 0, batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(), pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0, alpha_test_reference: 0,
}], }],
} }
@@ -992,8 +992,8 @@ mod static_mesh_tests {
material_index: 0, material_index: 0,
lightmap_index: u8::MAX, lightmap_index: u8::MAX,
batch_flags: 0, batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(), pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0, alpha_test_reference: 0,
}], }],
}; };
@@ -1023,8 +1023,8 @@ mod static_mesh_tests {
material_index: 7, material_index: 7,
lightmap_index: u8::MAX, lightmap_index: u8::MAX,
batch_flags: 0, batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(), pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0, alpha_test_reference: 0,
}, },
VulkanStaticDrawRange { VulkanStaticDrawRange {
@@ -1033,8 +1033,8 @@ mod static_mesh_tests {
material_index: 2, material_index: 2,
lightmap_index: u8::MAX, lightmap_index: u8::MAX,
batch_flags: 0, batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(), pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0, alpha_test_reference: 0,
}, },
]; ];
@@ -1101,6 +1101,11 @@ mod static_mesh_tests {
}, },
]; ];
assert_eq!(
resolve_draw_texture_indices(&ranges, &materials),
Ok(vec![1, 0])
);
let mut screen_tl = materials[0].clone(); let mut screen_tl = materials[0].clone();
screen_tl.screen_tl = true; screen_tl.screen_tl = true;
screen_tl.sky_far_depth = true; screen_tl.sky_far_depth = true;
@@ -1109,11 +1114,9 @@ mod static_mesh_tests {
far_sky.unlit = true; far_sky.unlit = true;
far_sky.sky_far_depth = true; far_sky.sky_far_depth = true;
assert_eq!(far_sky.encoded_material_mode(), 5.0); assert_eq!(far_sky.encoded_material_mode(), 5.0);
let mut nebula_with_stars = far_sky.clone();
assert_eq!( nebula_with_stars.sky_nebula_stars = true;
resolve_draw_texture_indices(&ranges, &materials), assert_eq!(nebula_with_stars.encoded_material_mode(), 6.0);
Ok(vec![1, 0])
);
let mut non_finite_base = materials.clone(); let mut non_finite_base = materials.clone();
non_finite_base[0].specular_rgb[0] = f32::NAN; non_finite_base[0].specular_rgb[0] = f32::NAN;
@@ -1149,6 +1152,7 @@ mod static_mesh_tests {
fn alpha_cutoff_is_dynamic_and_disabled_outside_alpha_test_pipeline() { fn alpha_cutoff_is_dynamic_and_disabled_outside_alpha_test_pipeline() {
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0]; let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
let disabled = VulkanStaticDrawRange { let disabled = VulkanStaticDrawRange {
terrain_base_only: false,
alpha_test_reference: 200, alpha_test_reference: 200,
..base ..base
}; };
@@ -1157,6 +1161,7 @@ mod static_mesh_tests {
alpha_test: true, alpha_test: true,
..LegacyPipelineState::default() ..LegacyPipelineState::default()
}, },
terrain_base_only: false,
alpha_test_reference: 128, alpha_test_reference: 128,
..base ..base
}; };
@@ -1167,6 +1172,7 @@ mod static_mesh_tests {
alpha_test: true, alpha_test: true,
..LegacyPipelineState::default() ..LegacyPipelineState::default()
}, },
terrain_base_only: false,
alpha_test_reference: 0, alpha_test_reference: 0,
..base ..base
}; };
@@ -1176,6 +1182,7 @@ mod static_mesh_tests {
alpha_test: true, alpha_test: true,
..LegacyPipelineState::default() ..LegacyPipelineState::default()
}, },
terrain_base_only: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
..base ..base
}; };
@@ -1472,8 +1479,8 @@ pub struct VulkanSmokeRenderer {
pub(super) material_selectors: Vec<u16>, pub(super) material_selectors: Vec<u16>,
/// Whether the host-side specular records need a fence-safe GPU upload. /// Whether the host-side specular records need a fence-safe GPU upload.
pub(super) material_specular_dirty: bool, pub(super) material_specular_dirty: bool,
/// Encoded material mode pushed at offset 124: 0 lit, 1 unlit, 2 native /// Encoded material mode pushed at offset 124: low bits select the
/// sky nebula/stars texture composite, 3 native lightmap composite. /// combiner, bit 4 selects sky far-depth, and bit 8 selects screen/TL.
pub(super) material_unlit: Vec<f32>, pub(super) material_unlit: Vec<f32>,
pub(super) readback_enabled: bool, pub(super) readback_enabled: bool,
pub(super) camera: VulkanStaticCamera, pub(super) camera: VulkanStaticCamera,
@@ -3,7 +3,8 @@
use ash::vk; use ash::vk;
use fparkan_platform::DepthStencilSupport; use fparkan_platform::DepthStencilSupport;
use fparkan_render::{ use fparkan_render::{
LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState, PipelineKey, LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
PipelineKey,
}; };
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -935,6 +936,14 @@ pub(super) fn draw_viewport(extent: (u32, u32), material_mode: f32) -> vk::Viewp
} }
} }
fn depth_bias_parameters(depth_bias: LegacyDepthBias) -> (bool, f32, f32) {
match depth_bias {
LegacyDepthBias::Disabled => (false, 0.0, 0.0),
// shortcut: Vulkan constantFactor=-1.0/slopeFactor=0.0 is empirical; verify native raster state before claiming parity.
LegacyDepthBias::ProjectedDecal => (true, -1.0, 0.0),
}
}
fn create_graphics_pipeline_cache( fn create_graphics_pipeline_cache(
device: &VulkanLogicalDeviceProbe, device: &VulkanLogicalDeviceProbe,
render_pass: vk::RenderPass, render_pass: vk::RenderPass,
@@ -1053,6 +1062,8 @@ fn create_graphics_pipeline(
let viewport_state = vk::PipelineViewportStateCreateInfo::default() let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewports(&viewports) .viewports(&viewports)
.scissors(&scissors); .scissors(&scissors);
let (depth_bias_enable, depth_bias_constant_factor, depth_bias_slope_factor) =
depth_bias_parameters(state.depth_bias);
let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default() let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
.depth_clamp_enable(false) .depth_clamp_enable(false)
.rasterizer_discard_enable(false) .rasterizer_discard_enable(false)
@@ -1064,7 +1075,10 @@ fn create_graphics_pipeline(
LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT, LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT,
}) })
.front_face(vk::FrontFace::CLOCKWISE) .front_face(vk::FrontFace::CLOCKWISE)
.depth_bias_enable(false); .depth_bias_enable(depth_bias_enable)
.depth_bias_constant_factor(depth_bias_constant_factor)
.depth_bias_slope_factor(depth_bias_slope_factor)
.depth_bias_clamp(0.0);
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default() let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
.sample_shading_enable(false) .sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1); .rasterization_samples(vk::SampleCountFlags::TYPE_1);
@@ -1337,7 +1351,20 @@ fn destroy_partial_swapchain_resources(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::draw_viewport; use super::{depth_bias_parameters, draw_viewport};
use fparkan_render::LegacyDepthBias;
#[test]
fn projected_decal_bias_parameters_use_empirical_vulkan_factors() {
assert_eq!(
depth_bias_parameters(LegacyDepthBias::Disabled),
(false, 0.0, 0.0)
);
assert_eq!(
depth_bias_parameters(LegacyDepthBias::ProjectedDecal),
(true, -1.0, 0.0)
);
}
#[test] #[test]
fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() { fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() {
+41 -5
View File
@@ -361,6 +361,17 @@ pub enum LegacyCullMode {
FrontFace, FrontFace,
} }
/// Raster depth adjustment applied to one pipeline variant.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum LegacyDepthBias {
/// Preserve the interpolated depth exactly.
#[default]
Disabled,
/// Move projected decals toward the camera so they pass against their
/// coplanar receiver under a less-or-equal depth test.
ProjectedDecal,
}
/// Legacy fixed-function state that changes graphics-pipeline structure. /// Legacy fixed-function state that changes graphics-pipeline structure.
/// ///
/// Alpha reference is deliberately not present: it is dynamic material data, /// Alpha reference is deliberately not present: it is dynamic material data,
@@ -375,6 +386,8 @@ pub struct LegacyPipelineState {
pub cull: LegacyCullMode, pub cull: LegacyCullMode,
/// Whether alpha-test shader logic is enabled. /// Whether alpha-test shader logic is enabled.
pub alpha_test: bool, pub alpha_test: bool,
/// Raster depth bias for projected decals.
pub depth_bias: LegacyDepthBias,
} }
impl LegacyPipelineState { impl LegacyPipelineState {
@@ -405,15 +418,16 @@ impl LegacyPipelineState {
/// Canonical, backend-neutral key for a graphics-pipeline variant. /// Canonical, backend-neutral key for a graphics-pipeline variant.
/// ///
/// The value is explicitly packed rather than hashed, so captures and caches /// The value is explicitly packed rather than hashed, so captures and caches
/// remain stable across processes and Rust toolchain updates. Three bits are /// remain stable across processes and Rust toolchain updates. Bits 0..7 encode
/// reserved for the five native material blend modes. /// blend, depth, cull and alpha-test state; bit 8 distinguishes projected
/// decal depth bias.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PipelineKey(u8); pub struct PipelineKey(u16);
impl PipelineKey { impl PipelineKey {
/// Returns the canonical packed representation. /// Returns the canonical packed representation.
#[must_use] #[must_use]
pub const fn packed(self) -> u8 { pub const fn packed(self) -> u16 {
self.0 self.0
} }
} }
@@ -437,7 +451,11 @@ impl From<LegacyPipelineState> for PipelineKey {
LegacyCullMode::BackFace => 1, LegacyCullMode::BackFace => 1,
LegacyCullMode::FrontFace => 2, LegacyCullMode::FrontFace => 2,
}; };
Self(blend | (depth << 3) | (cull << 5) | (u8::from(state.alpha_test) << 7)) let depth_bias = match state.depth_bias {
LegacyDepthBias::Disabled => 0,
LegacyDepthBias::ProjectedDecal => 1 << 8,
};
Self(blend | (depth << 3) | (cull << 5) | (u16::from(state.alpha_test) << 7) | depth_bias)
} }
} }
@@ -613,6 +631,24 @@ mod tests {
assert_eq!(base.with_material_category(5), None); assert_eq!(base.with_material_category(5), None);
} }
#[test]
fn projected_decal_bias_is_a_distinct_preserved_pipeline_state() {
let ordinary = LegacyPipelineState::default();
let decal = LegacyPipelineState {
depth_bias: LegacyDepthBias::ProjectedDecal,
..ordinary
};
assert_eq!(ordinary.depth_bias, LegacyDepthBias::Disabled);
assert_ne!(PipelineKey::from(ordinary), PipelineKey::from(decal));
assert_eq!(
decal
.with_material_category(1)
.expect("native category is represented")
.depth_bias,
LegacyDepthBias::ProjectedDecal
);
}
#[test] #[test]
fn native_material_categories_have_distinct_pipeline_keys() { fn native_material_categories_have_distinct_pipeline_keys() {
let base = LegacyPipelineState { let base = LegacyPipelineState {