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,
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::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]
fn material_category_and_transparency_preserve_world_state_in_either_order() {
let original = LegacyPipelineState {
@@ -3151,6 +3214,7 @@ mod tests {
depth: LegacyDepthMode::TestWrite,
cull: LegacyCullMode::FrontFace,
alpha_test: false,
depth_bias: LegacyDepthBias::ProjectedDecal,
};
let (base, category_first) =
world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
@@ -3162,14 +3226,17 @@ mod tests {
assert!(base.alpha_test);
assert_eq!(base.depth, LegacyDepthMode::TestWrite);
assert_eq!(base.cull, LegacyCullMode::FrontFace);
assert_eq!(base.depth_bias, LegacyDepthBias::ProjectedDecal);
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
assert_eq!(category_first.depth_bias, LegacyDepthBias::ProjectedDecal);
let fixed_state = LegacyPipelineState {
blend: LegacyBlendMode::Additive,
depth: LegacyDepthMode::Disabled,
cull: LegacyCullMode::Disabled,
alpha_test: false,
depth_bias: LegacyDepthBias::Disabled,
};
assert_eq!(
world_material_pipeline_states(fixed_state, 1, None),
@@ -756,8 +756,8 @@ impl VulkanStaticMesh {
material_index: 0,
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0,
}],
}
@@ -992,8 +992,8 @@ mod static_mesh_tests {
material_index: 0,
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0,
}],
};
@@ -1023,8 +1023,8 @@ mod static_mesh_tests {
material_index: 7,
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
alpha_test_reference: 0,
},
VulkanStaticDrawRange {
@@ -1033,8 +1033,8 @@ mod static_mesh_tests {
material_index: 2,
lightmap_index: u8::MAX,
batch_flags: 0,
terrain_base_only: false,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
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();
screen_tl.screen_tl = true;
screen_tl.sky_far_depth = true;
@@ -1109,11 +1114,9 @@ mod static_mesh_tests {
far_sky.unlit = true;
far_sky.sky_far_depth = true;
assert_eq!(far_sky.encoded_material_mode(), 5.0);
assert_eq!(
resolve_draw_texture_indices(&ranges, &materials),
Ok(vec![1, 0])
);
let mut nebula_with_stars = far_sky.clone();
nebula_with_stars.sky_nebula_stars = true;
assert_eq!(nebula_with_stars.encoded_material_mode(), 6.0);
let mut non_finite_base = materials.clone();
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() {
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
let disabled = VulkanStaticDrawRange {
terrain_base_only: false,
alpha_test_reference: 200,
..base
};
@@ -1157,6 +1161,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
terrain_base_only: false,
alpha_test_reference: 128,
..base
};
@@ -1167,6 +1172,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
terrain_base_only: false,
alpha_test_reference: 0,
..base
};
@@ -1176,6 +1182,7 @@ mod static_mesh_tests {
alpha_test: true,
..LegacyPipelineState::default()
},
terrain_base_only: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
..base
};
@@ -1472,8 +1479,8 @@ pub struct VulkanSmokeRenderer {
pub(super) material_selectors: Vec<u16>,
/// Whether the host-side specular records need a fence-safe GPU upload.
pub(super) material_specular_dirty: bool,
/// Encoded material mode pushed at offset 124: 0 lit, 1 unlit, 2 native
/// sky nebula/stars texture composite, 3 native lightmap composite.
/// Encoded material mode pushed at offset 124: low bits select the
/// combiner, bit 4 selects sky far-depth, and bit 8 selects screen/TL.
pub(super) material_unlit: Vec<f32>,
pub(super) readback_enabled: bool,
pub(super) camera: VulkanStaticCamera,
@@ -3,7 +3,8 @@
use ash::vk;
use fparkan_platform::DepthStencilSupport;
use fparkan_render::{
LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState, PipelineKey,
LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
PipelineKey,
};
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(
device: &VulkanLogicalDeviceProbe,
render_pass: vk::RenderPass,
@@ -1053,6 +1062,8 @@ fn create_graphics_pipeline(
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewports(&viewports)
.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()
.depth_clamp_enable(false)
.rasterizer_discard_enable(false)
@@ -1064,7 +1075,10 @@ fn create_graphics_pipeline(
LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT,
})
.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()
.sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
@@ -1337,7 +1351,20 @@ fn destroy_partial_swapchain_resources(
#[cfg(test)]
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]
fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() {
+41 -5
View File
@@ -361,6 +361,17 @@ pub enum LegacyCullMode {
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.
///
/// Alpha reference is deliberately not present: it is dynamic material data,
@@ -375,6 +386,8 @@ pub struct LegacyPipelineState {
pub cull: LegacyCullMode,
/// Whether alpha-test shader logic is enabled.
pub alpha_test: bool,
/// Raster depth bias for projected decals.
pub depth_bias: LegacyDepthBias,
}
impl LegacyPipelineState {
@@ -405,15 +418,16 @@ impl LegacyPipelineState {
/// Canonical, backend-neutral key for a graphics-pipeline variant.
///
/// The value is explicitly packed rather than hashed, so captures and caches
/// remain stable across processes and Rust toolchain updates. Three bits are
/// reserved for the five native material blend modes.
/// remain stable across processes and Rust toolchain updates. Bits 0..7 encode
/// blend, depth, cull and alpha-test state; bit 8 distinguishes projected
/// decal depth bias.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PipelineKey(u8);
pub struct PipelineKey(u16);
impl PipelineKey {
/// Returns the canonical packed representation.
#[must_use]
pub const fn packed(self) -> u8 {
pub const fn packed(self) -> u16 {
self.0
}
}
@@ -437,7 +451,11 @@ impl From<LegacyPipelineState> for PipelineKey {
LegacyCullMode::BackFace => 1,
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);
}
#[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]
fn native_material_categories_have_distinct_pipeline_keys() {
let base = LegacyPipelineState {