diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
3 files changed, 285 insertions, 34 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index a2481f7..0e1cebb 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -423,6 +423,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()], + draw_range_diffuse_alpha_overrides: vec![None; create_info.mesh.draw_ranges.len()], world_transparent_background_ranges: Vec::new(), draw_range_base_pipeline_states: create_info .mesh @@ -901,6 +902,27 @@ impl VulkanSmokeRenderer { Ok(()) } + /// Overrides one draw's diffuse alpha without changing its shared material. + pub fn set_draw_range_diffuse_alpha_override( + &mut self, + range_index: usize, + diffuse_alpha: Option<f32>, + ) -> Result<(), VulkanSmokeRendererError> { + if diffuse_alpha.is_some_and(|alpha| !alpha.is_finite() || !(0.0..=1.0).contains(&alpha)) { + return Err(VulkanSmokeRendererError::InvalidStaticMesh { + context: "dynamic draw-range diffuse alpha must be finite and 0..1", + }); + } + let alpha_override = self + .draw_range_diffuse_alpha_overrides + .get_mut(range_index) + .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { + context: "dynamic draw-range alpha index is out of bounds", + })?; + *alpha_override = diffuse_alpha; + Ok(()) + } + /// Sets the native per-range alpha reference in legacy 0..=255 units. /// /// Zero remains an explicit value; use the native-default constant when @@ -1808,6 +1830,7 @@ impl VulkanSmokeRenderer { 0, vk::IndexType::UINT32, ); + let mut viewport_depth = None; for &range_index in &self.draw_order { let range = self.draw_ranges.get(range_index).ok_or( VulkanSmokeRendererError::InvariantViolation { @@ -1873,11 +1896,16 @@ impl VulkanSmokeRenderer { context: "static material alpha", }, )?; - let material_alphas = [ - material_alpha[0].to_ne_bytes(), - material_alpha[1].to_ne_bytes(), - ] - .concat(); + let diffuse_alpha_override = self + .draw_range_diffuse_alpha_overrides + .get(range_index) + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "draw-range diffuse-alpha override", + })?; + let material_alphas = + draw_material_alphas(*material_alpha, *diffuse_alpha_override) + .map(f32::to_ne_bytes) + .concat(); device.device().cmd_push_constants( command_buffer, resources.pipeline_layout, @@ -1929,6 +1957,17 @@ impl VulkanSmokeRenderer { } else { *material_mode }; + let viewport = super::swapchain_resources::draw_viewport( + swapchain.report.plan.extent, + material_mode, + ); + let next_viewport_depth = [viewport.min_depth, viewport.max_depth]; + if viewport_depth != Some(next_viewport_depth) { + device + .device() + .cmd_set_viewport(command_buffer, 0, &[viewport]); + viewport_depth = Some(next_viewport_depth); + } let material_mode = [material_mode.to_ne_bytes()]; device.device().cmd_push_constants( command_buffer, @@ -2356,6 +2395,16 @@ fn apply_world_transparency( } } +fn draw_material_alphas( + material_alphas: [f32; 2], + diffuse_alpha_override: Option<f32>, +) -> [f32; 2] { + [ + diffuse_alpha_override.unwrap_or(material_alphas[0]), + material_alphas[1], + ] +} + fn world_material_pipeline_states( original: LegacyPipelineState, category: u8, @@ -2523,12 +2572,14 @@ mod tests { use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh}; use super::{ apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index, - mark_draw_order_dirty_on_activity_change, set_and_mark_dirty_if_changed, - sorted_world_draw_order, take_runtime_children_with_validation_snapshot, - take_runtime_owners_in_dependency_order, validate_geometry_upload, - world_material_pipeline_states, RollbackOnDrop, + draw_material_alphas, mark_draw_order_dirty_on_activity_change, + set_and_mark_dirty_if_changed, 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, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState, }; - use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState}; use std::cell::RefCell; use std::rc::Rc; @@ -3096,12 +3147,74 @@ 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 { blend: LegacyBlendMode::Opaque, 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"); @@ -3113,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), @@ -3129,4 +3245,22 @@ mod tests { assert!(world_material_pipeline_states(original, 5, Some(false)).is_none()); assert!(world_material_pipeline_states(fixed_state, 5, None).is_none()); } + + #[test] + fn draw_alpha_override_is_per_range_and_none_restores_material_alpha() { + let shared_material = [0.25, 0.5]; + + assert_eq!(draw_material_alphas(shared_material, Some(0.0)), [0.0, 0.5]); + assert_eq!(draw_material_alphas(shared_material, None), shared_material); + + let updated_material = [0.75, 0.125]; + assert_eq!( + draw_material_alphas(updated_material, None), + updated_material + ); + assert_eq!( + draw_material_alphas(updated_material, Some(0.0)), + [0.0, 0.125] + ); + } } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 51f8384..0996fa1 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -284,6 +284,9 @@ impl VulkanFrameUniforms { .chain([self.fog_start, self.fog_end]) .chain(self.camera_position) .all(f32::is_finite) + && (self.fog_end <= self.fog_start + || ((self.fog_start * self.fog_start).is_finite() + && (self.fog_end * self.fog_end).is_finite())) } /// Serializes one frame slot in the exact `std140` member order. @@ -344,7 +347,17 @@ impl VulkanFrameUniforms { self.fog_color[2], 0.0, ]); - values[84..88].copy_from_slice(&[self.fog_start, self.fog_end, 0.0, 0.0]); + let squared_fog_range = if self.fog_end > self.fog_start { + [self.fog_start * self.fog_start, self.fog_end * self.fog_end] + } else { + [0.0; 2] + }; + values[84..88].copy_from_slice(&[ + self.fog_start, + self.fog_end, + squared_fog_range[0], + squared_fog_range[1], + ]); values[88..92].copy_from_slice(&[ self.camera_position[0], self.camera_position[1], @@ -756,8 +769,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, }], } @@ -875,6 +888,31 @@ mod static_mesh_tests { } #[test] + fn fog_uniforms_match_live_native_squared_distance_range() { + // Terrain ShadeIndexedStrided: far=0x442389e6, far²=0x48d0f1ca; + // distance²=0x47707b04 gives visibility=0x3f5b2b94. + let frame = VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::from_bits(0x4423_89e6), + ..Default::default() + }; + assert!(frame.is_finite()); + let bytes = frame.to_ne_bytes(); + let read = |offset| f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + assert_eq!(read(344), 0.0); + assert_eq!(read(348).to_bits(), 0x48d0_f1ca); + let visibility = 1.0 - (f32::from_bits(0x4770_7b04) - read(344)) / (read(348) - read(344)); + assert!((visibility - f32::from_bits(0x3f5b_2b94)).abs() < 1.0e-6); + assert!(VulkanFrameUniforms::default().is_finite()); + assert!(!VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::MAX, + ..frame + } + .is_finite()); + } + + #[test] fn static_camera_composes_recovered_d3d7_view_before_projection() { let transform = RawCameraTransform { words: [ @@ -992,8 +1030,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 +1061,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 +1071,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 +1139,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 +1152,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 +1190,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 +1199,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: 128, ..base }; @@ -1167,6 +1210,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: 0, ..base }; @@ -1176,6 +1220,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, ..base }; @@ -1456,6 +1501,9 @@ 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>>, + /// Optional diffuse-alpha override for each draw range, independent of + /// the shared material record. + pub(super) draw_range_diffuse_alpha_overrides: Vec<Option<f32>>, /// 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. @@ -1469,8 +1517,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, diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index 89ca43a..973e8b1 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -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; @@ -922,8 +923,25 @@ fn create_texture_descriptor_bundle( Ok((layout, pool, descriptor_sets, sampler)) } -fn extent_component_to_f32(value: u32) -> f32 { - value as f32 +pub(super) fn draw_viewport(extent: (u32, u32), material_mode: f32) -> vk::Viewport { + // Ngi32 RVA 0x7170 maps world depth to 0.1..0.99; TL and sky depth bypass it. + let explicit_depth = material_mode.round() as u32 & 12 != 0; + vk::Viewport { + x: 0.0, + y: 0.0, + width: extent.0 as f32, + height: extent.1 as f32, + min_depth: if explicit_depth { 0.0 } else { 0.1 }, + max_depth: if explicit_depth { 1.0 } else { 0.99 }, + } +} + +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( @@ -1033,14 +1051,7 @@ fn create_graphics_pipeline( let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default() .topology(vk::PrimitiveTopology::TRIANGLE_LIST) .primitive_restart_enable(false); - let viewports = [vk::Viewport { - x: 0.0, - y: 0.0, - width: extent_component_to_f32(extent.0), - height: extent_component_to_f32(extent.1), - min_depth: 0.0, - max_depth: 1.0, - }]; + let viewports = [draw_viewport(extent, 0.0)]; let scissors = [vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent: vk::Extent2D { @@ -1051,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) @@ -1062,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); @@ -1105,6 +1121,9 @@ fn create_graphics_pipeline( let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default() .logic_op_enable(false) .attachments(&color_blend_attachments); + let dynamic_states = [vk::DynamicState::VIEWPORT]; + let dynamic_state = + vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); let create_info = vk::GraphicsPipelineCreateInfo::default() .stages(&shader_stages) .vertex_input_state(&vertex_input_state) @@ -1114,6 +1133,7 @@ fn create_graphics_pipeline( .multisample_state(&multisample_state) .depth_stencil_state(&depth_stencil_state) .color_blend_state(&color_blend_state) + .dynamic_state(&dynamic_state) .layout(pipeline_layout) .render_pass(render_pass) .subpass(0); @@ -1328,3 +1348,52 @@ fn destroy_partial_swapchain_resources( } } } + +#[cfg(test)] +mod tests { + 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() { + let projection = fparkan_render::LegacyD3d7Projection { + viewport: [0, 0, 1280, 1024], + near_plane: 0.5, + far_plane: 700.0, + field_of_view_radians: 1.04, + } + .try_direct3d7_projection_row_major() + .expect("native projection"); + for mode in [0.0, 1.0, 2.0, 3.0, 16.0, 17.0] { + let viewport = draw_viewport((1280, 1024), mode); + assert_eq!([viewport.width, viewport.height], [1280.0, 1024.0]); + assert_eq!([viewport.min_depth, viewport.max_depth], [0.1, 0.99]); + for distance in [0.5, 100.0, 350.0, 600.0, 700.0] { + let clip_z = distance * projection[10] + projection[14]; + let clip_w = distance * projection[11]; + let depth = viewport.min_depth + + (viewport.max_depth - viewport.min_depth) * (clip_z / clip_w); + assert!( + depth < 0.999, + "mode {mode}, distance {distance}, depth {depth}" + ); + } + } + for mode in [4.0, 5.0, 6.0, 7.0, 9.0] { + let viewport = draw_viewport((640, 480), mode); + assert_eq!([viewport.min_depth, viewport.max_depth], [0.0, 1.0]); + } + } +} |
