Fix material categories and transparent background ordering
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s

This commit is contained in:
2026-10-11 21:41:37 +04:00
parent bfe4db1611
commit c90ec80dc0
6 changed files with 496 additions and 53 deletions
+276 -37
View File
@@ -1,7 +1,7 @@
#![allow(unsafe_code)]
use ash::vk;
use fparkan_render::LegacyDepthMode;
use fparkan_render::{LegacyDepthMode, LegacyPipelineState};
use super::smoke_types::resolve_draw_texture_indices;
use super::update_material_texture_descriptor;
@@ -434,6 +434,7 @@ impl VulkanSmokeRenderer {
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
draw_order_dirty: false,
draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()],
world_transparent_background_ranges: Vec::new(),
draw_range_base_pipeline_states: create_info
.mesh
.draw_ranges
@@ -878,6 +879,74 @@ impl VulkanSmokeRenderer {
Ok(())
}
/// Applies the active MAT0 phase's blend and alpha-test category to one
/// range while retaining its authored depth and cull state.
pub fn set_draw_range_material_category(
&mut self,
range_index: usize,
category: u8,
) -> Result<(), VulkanSmokeRendererError> {
let base_state = *self
.draw_range_base_pipeline_states
.get(range_index)
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
context: "dynamic draw range index is out of bounds",
})?;
let world_queue = self
.draw_range_world_queue
.get(range_index)
.copied()
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
context: "dynamic draw range index is out of bounds",
})?;
// Fixed sky, sprite, weather, and shadow ranges own explicit states.
// MAT0 categories apply only after a range has entered the world queue.
let (next_base, next_state) =
world_material_pipeline_states(base_state, category, world_queue).ok_or(
VulkanSmokeRendererError::InvalidStaticMesh {
context: "MAT0 pipeline category is outside the native 0..=4 table",
},
)?;
let range = self.draw_ranges.get_mut(range_index).ok_or(
VulkanSmokeRendererError::InvalidStaticMesh {
context: "dynamic draw range index is out of bounds",
},
)?;
self.draw_range_base_pipeline_states[range_index] = next_base;
let pipeline_changed = range.pipeline_state != next_state;
range.pipeline_state = next_state;
if pipeline_changed {
self.rebuild_pipeline_resources()?;
}
Ok(())
}
/// Places nebula, dome, and cloud ranges between opaque and transparent
/// world draws. The list is explicit so unrelated fixed passes keep their
/// established order.
pub fn set_world_transparent_background_ranges(
&mut self,
range_indices: &[usize],
) -> Result<(), VulkanSmokeRendererError> {
let mut seen = vec![false; self.draw_ranges.len()];
for &range_index in range_indices {
let Some(slot) = seen.get_mut(range_index) else {
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
context: "world background range index is out of bounds",
});
};
if *slot || self.draw_range_world_queue[range_index].is_some() {
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
context: "world background range is duplicated or already queued",
});
}
*slot = true;
}
self.world_transparent_background_ranges = range_indices.to_vec();
self.draw_order_dirty = true;
Ok(())
}
/// Marks a draw range as belonging to the native transparent queue.
///
/// The flag is deliberately independent of the blend category: native
@@ -886,14 +955,25 @@ impl VulkanSmokeRenderer {
/// depth attachment switches to depth-test/read-only. The original depth
/// state is retained so disabling the flag restores the caller's state.
///
/// The transparent flag only participates in the world draw order. The
/// backend keeps every unmarked range in its original slot, which leaves
/// the fixed screen/sky/weather pass order intact.
/// The transparent flag only participates in the world draw order. Without
/// explicit backgrounds, opaque and transparent ranges replace the
/// existing world slots. With explicit backgrounds, opaque ranges compact
/// into those slots and the sorted transparent ranges are inserted after
/// the last background; unrelated fixed ranges keep relative order but
/// may shift slots.
pub fn set_draw_range_transparency(
&mut self,
range_index: usize,
transparent: bool,
) -> Result<(), VulkanSmokeRendererError> {
if self
.world_transparent_background_ranges
.contains(&range_index)
{
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
context: "world background range cannot join the world queue",
});
}
let base_state = *self
.draw_range_base_pipeline_states
.get(range_index)
@@ -905,13 +985,7 @@ impl VulkanSmokeRenderer {
context: "dynamic draw range index is out of bounds",
},
)?;
let depth = if transparent && base_state.depth != LegacyDepthMode::Disabled {
LegacyDepthMode::TestReadOnly
} else {
base_state.depth
};
let mut next_state = base_state;
next_state.depth = depth;
let next_state = apply_world_transparency(base_state, transparent);
let pipeline_changed = range.pipeline_state != next_state;
range.pipeline_state = next_state;
// Calling this setter opts the range into the world queue. Fixed
@@ -1499,11 +1573,13 @@ impl VulkanSmokeRenderer {
self.rebuild_swapchain_resources(true)
}
/// Recomputes the stable command-recording order. Only ranges explicitly
/// marked transparent are reordered, and they remain in the slots occupied
/// by that queue. This preserves the caller's fixed sky/screen/weather
/// pass order while matching the native descending key comparator for the
/// world transparent queue.
/// Recomputes the stable command-recording order. Without explicit
/// backgrounds, opaque world ranges followed by the sorted transparent
/// world ranges replace the existing world slots. With explicit
/// backgrounds, opaque ranges compact into the world slots and sorted
/// transparent ranges are inserted after the last background; unrelated
/// fixed ranges keep relative order but may shift slots. The transparent
/// queue matches the native descending key comparator.
fn rebuild_draw_order(&mut self) {
let range_count = self.draw_ranges.len();
if self.draw_order.len() != range_count {
@@ -1514,6 +1590,7 @@ impl VulkanSmokeRenderer {
&self.draw_ranges,
&self.draw_range_world_queue,
&self.draw_range_sort_keys,
&self.world_transparent_background_ranges,
);
}
@@ -2109,15 +2186,46 @@ fn apply_geometry_range_updates(
}
}
fn apply_world_transparency(
base_state: LegacyPipelineState,
transparent: bool,
) -> LegacyPipelineState {
if transparent && base_state.depth != LegacyDepthMode::Disabled {
LegacyPipelineState {
depth: LegacyDepthMode::TestReadOnly,
..base_state
}
} else {
base_state
}
}
fn world_material_pipeline_states(
original: LegacyPipelineState,
category: u8,
world_queue: Option<bool>,
) -> Option<(LegacyPipelineState, LegacyPipelineState)> {
let categorized = original.with_material_category(category)?;
let Some(transparent) = world_queue else {
return Some((original, original));
};
let base = categorized;
Some((base, apply_world_transparency(base, transparent)))
}
/// Replays the native queue construction from the original stable range
/// order. Fixed passes stay in their original slots; world opaque ranges are
/// emitted first, followed by world transparent ranges inserted by descending
/// camera distance. Zero-index-count ranges are inactive and are omitted.
/// order. Without explicit background ranges, world opaque ranges replace the
/// existing world slots and world transparent ranges follow in descending
/// camera distance order. With explicit backgrounds, opaque ranges are
/// compacted into world positions and sorted transparent ranges are inserted
/// after the last active background; fixed passes keep their relative order but
/// can shift slots. Zero-index-count ranges are inactive and are omitted.
fn sorted_world_draw_order(
range_count: usize,
ranges: &[super::VulkanStaticDrawRange],
world_queue: &[Option<bool>],
keys: &[Option<f32>],
transparent_background_ranges: &[usize],
) -> Vec<usize> {
let mut next = (0..range_count).collect::<Vec<_>>();
let mut world_opaque = Vec::new();
@@ -2176,33 +2284,70 @@ fn sorted_world_draw_order(
ordered_transparent.insert(low, range_index);
}
let mut world_order = world_opaque;
world_order.extend(ordered_transparent);
let mut world_cursor = 0usize;
for slot in &mut next {
if world_queue.get(*slot).copied().flatten().is_some()
&& ranges
.get(*slot)
.is_some_and(|range| range.index_count != 0)
{
if let Some(&replacement) = world_order.get(world_cursor) {
*slot = replacement;
world_cursor += 1;
if transparent_background_ranges.is_empty() {
let mut world_order = world_opaque;
world_order.extend(ordered_transparent);
let mut world_cursor = 0usize;
for slot in &mut next {
if world_queue.get(*slot).copied().flatten().is_some()
&& ranges
.get(*slot)
.is_some_and(|range| range.index_count != 0)
{
if let Some(&replacement) = world_order.get(world_cursor) {
*slot = replacement;
world_cursor += 1;
}
}
}
return next;
}
next
// Fixed sky backgrounds are not world-queue members. Remove the world
// transparent slots, compact opaque ranges into the world slots, then
// insert the sorted transparent queue after the explicit background group.
let mut opaque_cursor = 0usize;
let mut next_without_transparency = Vec::with_capacity(range_count);
for slot in 0..range_count {
let active_world = ranges.get(slot).is_some_and(|range| range.index_count != 0);
match world_queue.get(slot).copied().flatten() {
Some(true) if active_world => {}
Some(false) if active_world => {
if let Some(&replacement) = world_opaque.get(opaque_cursor) {
next_without_transparency.push(replacement);
opaque_cursor += 1;
} else {
next_without_transparency.push(slot);
}
}
_ => next_without_transparency.push(slot),
}
}
let insertion = transparent_background_ranges
.iter()
.filter_map(|background| {
next_without_transparency
.iter()
.position(|range| range == background)
})
.max()
.map(|index| index + 1);
if let Some(insertion) = insertion {
next_without_transparency.splice(insertion..insertion, ordered_transparent);
}
next_without_transparency
}
#[cfg(test)]
mod tests {
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
use super::{
apply_geometry_range_updates, completed_readback_buffer_index, sorted_world_draw_order,
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
validate_geometry_upload, RollbackOnDrop,
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
sorted_world_draw_order, take_runtime_children_with_validation_snapshot,
take_runtime_owners_in_dependency_order, validate_geometry_upload,
world_material_pipeline_states, RollbackOnDrop,
};
use fparkan_render::{LegacyBlendMode, LegacyDepthMode, LegacyPipelineState};
use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState};
use std::cell::RefCell;
use std::rc::Rc;
@@ -2506,6 +2651,7 @@ mod tests {
&ranges,
&[None, Some(true), None, Some(true), Some(false)],
&[None, Some(2.0), None, Some(9.0), Some(-3.0)],
&[],
);
assert_eq!(order, vec![0, 4, 2, 3, 1]);
}
@@ -2546,6 +2692,7 @@ mod tests {
&ranges,
&[None, Some(true), Some(true)],
&[None, Some(100.0), Some(1.0)],
&[],
);
assert_eq!(order, vec![0, 1, 2]);
}
@@ -2563,7 +2710,99 @@ mod tests {
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
let order = sorted_world_draw_order(4, &ranges, &[Some(true); 4], &[Some(1.0); 4]);
let order = sorted_world_draw_order(4, &ranges, &[Some(true); 4], &[Some(1.0); 4], &[]);
assert_eq!(order, vec![2, 3, 1, 0]);
}
#[test]
fn tree_canopy_queue_follows_sky_and_precedes_foreground_effects() {
const SCREEN_GRADIENT: usize = 0;
const TERRAIN: usize = 1;
const CANOPY_NEAR: usize = 2;
const BUILDING: usize = 3;
const CANOPY_FAR: usize = 4;
const SHADOW: usize = 5;
const NEBULA: usize = 6;
const DOME: usize = 7;
const CLOUDS: usize = 8;
const SUN: usize = 9;
const WEATHER: usize = 10;
let 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(),
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
let mut world_queue = vec![None; ranges.len()];
world_queue[TERRAIN] = Some(false);
world_queue[CANOPY_NEAR] = Some(true);
world_queue[BUILDING] = Some(false);
world_queue[CANOPY_FAR] = Some(true);
let mut sort_keys = vec![None; ranges.len()];
sort_keys[CANOPY_NEAR] = Some(2.0);
sort_keys[CANOPY_FAR] = Some(9.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,
CANOPY_FAR,
CANOPY_NEAR,
SUN,
WEATHER,
]
);
}
#[test]
fn material_category_and_transparency_preserve_world_state_in_either_order() {
let original = LegacyPipelineState {
blend: LegacyBlendMode::Opaque,
depth: LegacyDepthMode::TestWrite,
cull: LegacyCullMode::FrontFace,
alpha_test: false,
};
let (base, category_first) =
world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
let transparency_first = apply_world_transparency(original, true)
.with_material_category(1)
.expect("category 1 after transparency");
assert_eq!(category_first, transparency_first);
assert_eq!(base.blend, LegacyBlendMode::SourceAlpha);
assert!(base.alpha_test);
assert_eq!(base.depth, LegacyDepthMode::TestWrite);
assert_eq!(base.cull, LegacyCullMode::FrontFace);
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
let fixed_state = LegacyPipelineState {
blend: LegacyBlendMode::Additive,
depth: LegacyDepthMode::Disabled,
cull: LegacyCullMode::Disabled,
alpha_test: false,
};
assert_eq!(
world_material_pipeline_states(fixed_state, 1, None),
Some((fixed_state, fixed_state))
);
assert!(world_material_pipeline_states(original, 5, Some(false)).is_none());
assert!(world_material_pipeline_states(fixed_state, 5, None).is_none());
}
}
@@ -449,6 +449,8 @@ impl VulkanStaticDrawRange {
pub struct VulkanStaticMaterial {
/// Positional material selector used by one or more source batches.
pub material_index: u16,
/// Native MAT0 pipeline category selected for this phase (0..=4).
pub pipeline_category: u8,
/// Decoded RGBA8 diffuse texture for this selector.
pub texture: VulkanStaticTexture,
/// Phase parameters 4..6, normalized directional light RGB coefficient.
@@ -1003,6 +1005,7 @@ mod static_mesh_tests {
let materials = [
VulkanStaticMaterial {
material_index: 2,
pipeline_category: 0,
texture: texture(),
detail_texture: None,
overlay_texture: None,
@@ -1028,6 +1031,7 @@ mod static_mesh_tests {
},
VulkanStaticMaterial {
material_index: 7,
pipeline_category: 0,
texture: texture(),
detail_texture: None,
overlay_texture: None,
@@ -1379,6 +1383,8 @@ pub struct VulkanSmokeRenderer {
/// a fixed screen/sky/weather pass; `Some(false)` is world opaque and
/// `Some(true)` is world transparent.
pub(super) draw_range_world_queue: Vec<Option<bool>>,
/// Fixed sky background ranges placed between world opaque and transparent.
pub(super) world_transparent_background_ranges: Vec<usize>,
/// Pipeline state before transparent queue depth policy is applied.
pub(super) draw_range_base_pipeline_states: Vec<LegacyPipelineState>,
pub(super) material_alphas: Vec<[f32; 2]>,