diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 313 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 6 |
2 files changed, 282 insertions, 37 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index e8e18ac..c3565e1 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -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; + } + + // 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), + } } - next + 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()); + } } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 75877ad..c41eff9 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -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]>, |
