diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
6 files changed, 190 insertions, 29 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/packet_distance.rs b/adapters/fparkan-render-vulkan/src/ffi/packet_distance.rs new file mode 100644 index 0000000..b6b8938 --- /dev/null +++ b/adapters/fparkan-render-vulkan/src/ffi/packet_distance.rs @@ -0,0 +1,52 @@ +#![allow(unsafe_code)] +//! The native CPU instruction used for render-packet distance keys. + +/// Reproduces Ngi32's squared length multiplied by `RSQRTSS` on x86-64. +/// Other architectures retain the portable distance and report that boundary. +#[must_use] +pub fn legacy_packet_distance_squared(squared: f32) -> Option<f32> { + if !squared.is_finite() || squared < 0.0 { + return None; + } + if squared == 0.0 { + static NOTICE: std::sync::Once = std::sync::Once::new(); + NOTICE.call_once(|| { + eprintln!("render_coverage packet_zero_distance_fallback=1 (stable zero instead of native 0 * infinity)"); + }); + return Some(0.0); + } + #[cfg(target_arch = "x86_64")] + let distance = { + use core::arch::x86_64::{_mm_cvtss_f32, _mm_rsqrt_ss, _mm_set_ss}; + // SAFETY: SSE is part of the x86-64 baseline; these intrinsics only + // operate on register values and do not access memory. + let inverse = unsafe { _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(squared))) }; + squared * inverse + }; + #[cfg(not(target_arch = "x86_64"))] + let distance = { + static NOTICE: std::sync::Once = std::sync::Once::new(); + NOTICE.call_once(|| { + eprintln!("render_coverage packet_distance_native_rsqrt_unsupported=1 (using portable square root)"); + }); + squared.sqrt() + }; + distance.is_finite().then_some(distance) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn packet_distance_preserves_the_native_approximation_error_bound() { + for squared in [0.000_001_f32, 1.0, 9.0, 11_393.854, 1_000_000.0] { + let distance = legacy_packet_distance_squared(squared).unwrap(); + assert!((distance / squared.sqrt() - 1.0).abs() < 0.000_3); + } + assert_eq!(legacy_packet_distance_squared(0.0), Some(0.0)); + for invalid in [-1.0, f32::NAN, f32::INFINITY] { + assert_eq!(legacy_packet_distance_squared(invalid), None); + } + } +} diff --git a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs index 62a0d1e..6eb6c67 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs @@ -221,6 +221,9 @@ impl ReflectionResources { depth_request: fparkan_platform::DepthStencilSupport, render_pass: vk::RenderPass, ) -> Result<(), VulkanSmokeRendererError> { + let side = fparkan_render::water::legacy_embm_reflection_size([extent.0, extent.1]); + let extent = (side, side); + eprintln!("render_reflection target_extent={side}x{side}"); let info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(vk::Format::from_raw(format)) @@ -236,7 +239,7 @@ impl ReflectionResources { .usage(vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED); - // SAFETY: This target uses the validated non-zero swapchain extent and format. + // SAFETY: The validated swapchain format is retained; the square side is in 2..=1024. let image = unsafe { self.device.create_image(&info, None) }.map_err(|result| { VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImage(reflection)", @@ -403,6 +406,7 @@ mod tests { ], indices: vec![0, 1, 2], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index 498a472..449750f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -687,12 +687,13 @@ pub(super) fn encode_point_lights( .chain(light.rgb) .chain([if light.active { 1.0 } else { 0.0 }]) .chain(light.coefficients) - .chain([if light.active { 1.0 } else { 0.0 }]) { bytes[offset..offset + std::mem::size_of::<f32>()] .copy_from_slice(&value.to_ne_bytes()); offset += std::mem::size_of::<f32>(); } + bytes[offset..offset + 4].copy_from_slice(&light.owner_scope.to_ne_bytes()); + offset += 4; } debug_assert_eq!(offset, total); Ok(bytes) diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index bd745ad..8499c54 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -373,7 +373,7 @@ impl VulkanSmokeRenderer { plane_heights.sort_unstable(); plane_heights.dedup(); if !plane_heights.is_empty() { - eprintln!("render_coverage planar_reflection_shifted_camera_unsupported={} (native bump map and clock; using a projective mirror until shifted-camera mapping is implemented)", plane_heights.len()); + eprintln!("render_coverage planar_reflection_render_context_approx={} (native shifted window, target size and bump UV; main-view depth range and sky/fog/FX remain approximations; non-perspective cameras use mirror fallback)", plane_heights.len()); eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1)); } if unsupported_nonplanar != 0 { @@ -1564,11 +1564,18 @@ impl VulkanSmokeRenderer { .active_reflection_plane() .map(|height| { let mut frame = self.frame_uniforms; - frame.clip_from_world = + frame.clip_from_world = fparkan_render::water::legacy_embm_reflection_clip( + frame.clip_from_world, + frame.camera_position, + height, + frame.embm_bounds, + ) + .or_else(|| { fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height) - .ok_or(VulkanSmokeRendererError::InvalidStaticCamera { - context: "non-finite reflected clip transform", - })?; + }) + .ok_or(VulkanSmokeRendererError::InvalidStaticCamera { + context: "non-finite reflected clip transform", + })?; frame.camera_position[2] = 2.0 * height - frame.camera_position[2]; if !frame.is_finite() { return Err(VulkanSmokeRendererError::InvalidStaticCamera { @@ -1819,6 +1826,15 @@ impl VulkanSmokeRenderer { self.draw_order_dirty = false; } let reflection_plane = self.active_reflection_plane(); + let shifted_reflection = reflection_plane.is_some_and(|height| { + fparkan_render::water::legacy_embm_reflection_clip( + self.frame_uniforms.clip_from_world, + self.frame_uniforms.camera_position, + height, + self.frame_uniforms.embm_bounds, + ) + .is_some() + }); let device = self.device_ref()?; let swapchain = self.swapchain_ref()?; let resources = self.resources_ref()?; @@ -1858,17 +1874,27 @@ impl VulkanSmokeRenderer { }, }, ]; - let render_area = vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent: vk::Extent2D { - width: swapchain.report.plan.extent.0, - height: swapchain.report.plan.extent.1, - }, - }; for reflected in [true, false] { if reflected && reflection_plane.is_none() { continue; } + let extent = if reflected { + let target = resources.reflection.color.as_ref().ok_or( + VulkanSmokeRendererError::InvariantViolation { + context: "active reflection without a color target", + }, + )?; + (target.width, target.height) + } else { + swapchain.report.plan.extent + }; + let render_area = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { + width: extent.0, + height: extent.1, + }, + }; let framebuffer = if reflected { resources.reflection.framebuffer } else { @@ -1910,7 +1936,16 @@ impl VulkanSmokeRenderer { &[], ); let mut viewport_depth = None; - for &range_index in &self.draw_order { + // Native reflection queues the translucent water after the reflected world. + let draw_order = + self.draw_order + .iter() + .copied() + .filter(|&index| !reflected || !self.draw_ranges[index].terrain_reflection) + .chain(self.draw_order.iter().copied().filter(|&index| { + reflected && self.draw_ranges[index].terrain_reflection + })); + for range_index in draw_order { let range = self.draw_ranges.get(range_index).ok_or( VulkanSmokeRendererError::InvariantViolation { context: "stable draw order range handle", @@ -1922,11 +1957,17 @@ impl VulkanSmokeRenderer { }, )?; let mode = self.material_unlit[texture_index].round() as i32; - if reflected && (range.terrain_reflection || mode & 8 != 0) { + if reflected && mode & 8 != 0 { continue; } let state = if reflected { - super::reflection::reflected_pipeline_state(range.pipeline_state) + let state = + super::reflection::reflected_pipeline_state(range.pipeline_state); + if range.terrain_reflection { + fparkan_render::water::legacy_reflected_water_pipeline(state) + } else { + state + } } else { range.pipeline_state }; @@ -1972,7 +2013,12 @@ impl VulkanSmokeRenderer { std::slice::from_ref(descriptor_set), &[frame_offset, material_offset, point_light_offset], ); - let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes(); + let alpha_cutoff = if state.alpha_test { + f32::from(range.alpha_test_reference) / 255.0 + } else { + 0.0 + } + .to_ne_bytes(); device.device().cmd_push_constants( command_buffer, resources.pipeline_layout, @@ -1991,8 +2037,13 @@ impl VulkanSmokeRenderer { .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "draw-range diffuse-alpha override", })?; + let diffuse_alpha_override = if reflected && range.terrain_reflection { + Some(fparkan_render::water::REFLECTED_WATER_ALPHA) + } else { + *diffuse_alpha_override + }; let material_alphas = - draw_material_alphas(*material_alpha, *diffuse_alpha_override) + draw_material_alphas(*material_alpha, diffuse_alpha_override) .map(f32::to_ne_bytes) .concat(); device.device().cmd_push_constants( @@ -2049,18 +2100,20 @@ impl VulkanSmokeRenderer { }; let material_mode = (material_mode.round() as i32 | if reflected { 32 } else { 0 } + | if reflected && range.terrain_reflection { + 256 + } else { + 0 + } | if !reflected && reflection_plane.is_some() && self.reflection_planes[range_index] == reflection_plane { - 64 + 64 | if shifted_reflection { 128 } else { 0 } } else { 0 }) as f32; - let viewport = super::swapchain_resources::draw_viewport( - swapchain.report.plan.extent, - material_mode, - ); + let viewport = super::swapchain_resources::draw_viewport(extent, material_mode); let next_viewport_depth = [viewport.min_depth, viewport.max_depth]; if viewport_depth != Some(next_viewport_depth) { device @@ -2082,7 +2135,7 @@ impl VulkanSmokeRenderer { 1, range.first_index, 0, - 0, + range.owner_scope, ); } device.device().cmd_end_render_pass(command_buffer); @@ -2875,6 +2928,7 @@ mod tests { }; let mut ranges = vec![ VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 7, @@ -2886,6 +2940,7 @@ mod tests { alpha_test_reference: 17, }, VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 9, @@ -3043,6 +3098,7 @@ mod tests { #[test] fn geometry_updates_dirty_world_order_on_activation_and_deactivation() { let mut ranges = [VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 0, material_index: 0, @@ -3230,6 +3286,7 @@ mod tests { fn native_transparent_order_is_descending_and_keeps_fixed_pass_slots() { let ranges = (0..5) .map(|_| super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -3255,6 +3312,7 @@ mod tests { fn inactive_world_range_is_not_reused_as_a_sort_slot() { let ranges = vec![ super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -3266,6 +3324,7 @@ mod tests { alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 0, material_index: 0, @@ -3277,6 +3336,7 @@ mod tests { alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 0, @@ -3302,6 +3362,7 @@ mod tests { fn native_equal_key_insertion_uses_inclusive_midpoint() { let ranges = (0..4) .map(|index| super::super::VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, @@ -3396,6 +3457,7 @@ mod tests { const WEATHER: usize = 10; let ranges = (0..11) .map(|index| VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, @@ -3455,6 +3517,7 @@ mod tests { const WEATHER: usize = 10; let mut ranges = (0..11) .map(|index| VulkanStaticDrawRange { + owner_scope: 0, first_index: index * 3, index_count: 3, material_index: 0, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 90fd7b4..9fd0582 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -158,6 +158,8 @@ impl Default for VulkanDirectionalLight { /// One point light in the frame lighting contract. #[derive(Clone, Copy, Debug, PartialEq)] pub struct VulkanPointLight { + /// Zero lights the world; a nonzero scope lights only matching object ranges. + pub owner_scope: u32, /// World-space position. pub position: [f32; 3], /// Light RGB multiplier. @@ -173,6 +175,7 @@ pub struct VulkanPointLight { impl Default for VulkanPointLight { fn default() -> Self { Self { + owner_scope: 0, position: [0.0; 3], rgb: [0.0; 3], range: 0.0, @@ -201,7 +204,7 @@ pub const VULKAN_FRAME_SLOT_COUNT: usize = 2; /// `uvec4` header bytes for the dynamic point-light storage buffer. pub const VULKAN_POINT_LIGHT_HEADER_BYTES: usize = 16; -/// Three `vec4` records per dynamically uploaded point light. +/// Two vec4 records, three attenuation floats and one u32 owner scope. pub const VULKAN_POINT_LIGHT_RECORD_BYTES: usize = 3 * 4 * std::mem::size_of::<f32>(); /// Bytes occupied by one material's base and overlay specular records. @@ -444,6 +447,8 @@ pub struct VulkanDynamicDrawRange { /// One indexed triangle-list draw retained from an original mesh batch. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct VulkanStaticDrawRange { + /// Root-object lighting scope; zero denotes terrain or unowned geometry. + pub owner_scope: u32, /// First index in the shared index buffer. pub first_index: u32, /// Number of indices in this draw. @@ -787,6 +792,7 @@ impl VulkanStaticMesh { ], indices: vec![0, 1, 2], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 0, @@ -872,6 +878,7 @@ mod static_mesh_tests { VulkanDirectionalLight::default(), ], point_light: VulkanPointLight { + owner_scope: 0, position: [28.0, 29.0, 30.0], rgb: [31.0, 32.0, 33.0], range: 34.0, @@ -1073,6 +1080,7 @@ mod static_mesh_tests { vertices: VulkanStaticMesh::smoke_triangle().vertices, indices: vec![0, 1], draw_ranges: vec![VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 2, material_index: 0, @@ -1105,6 +1113,7 @@ mod static_mesh_tests { fn material_descriptors_follow_source_batch_selectors() { let ranges = [ VulkanStaticDrawRange { + owner_scope: 0, first_index: 0, index_count: 3, material_index: 7, @@ -1116,6 +1125,7 @@ mod static_mesh_tests { alpha_test_reference: 0, }, VulkanStaticDrawRange { + owner_scope: 0, first_index: 3, index_count: 3, material_index: 2, diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index b06bb26..8dda737 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -419,6 +419,32 @@ fn swapchain_plan_enables_transfer_source_when_surface_supports_it() { } #[test] +fn swapchain_plan_falls_back_to_immediate_without_overriding_supported_preference() { + let mut request = swapchain_request(); + request.present_modes = vec![ + vk::PresentModeKHR::IMMEDIATE.as_raw(), + vk::PresentModeKHR::FIFO.as_raw(), + ]; + assert_eq!( + plan_vulkan_swapchain(&request).unwrap().present_mode, + vk::PresentModeKHR::IMMEDIATE.as_raw() + ); + request.preferred_present_mode = vk::PresentModeKHR::FIFO.as_raw(); + assert_eq!( + plan_vulkan_swapchain(&request).unwrap().present_mode, + vk::PresentModeKHR::FIFO.as_raw() + ); + request + .present_modes + .push(vk::PresentModeKHR::MAILBOX.as_raw()); + request.preferred_present_mode = vk::PresentModeKHR::MAILBOX.as_raw(); + assert_eq!( + plan_vulkan_swapchain(&request).unwrap().present_mode, + vk::PresentModeKHR::MAILBOX.as_raw() + ); +} + +#[test] fn swapchain_plan_uses_fifo_and_current_extent_fallbacks() { let mut request = swapchain_request(); request.preferred_present_mode = vk::PresentModeKHR::IMMEDIATE.as_raw(); @@ -492,8 +518,9 @@ fn fragment_shader_dispatches_all_four_material_modes() { } #[test] -fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { +fn point_light_storage_payload_preserves_integer_owner_scope_and_stride() { let lights = [VulkanPointLight { + owner_scope: 0x1234_5678, position: [1.0, 2.0, 3.0], rgb: [4.0, 5.0, 6.0], range: 7.0, @@ -506,7 +533,7 @@ fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { VULKAN_POINT_LIGHT_HEADER_BYTES + VULKAN_POINT_LIGHT_RECORD_BYTES ); assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 1); - let values = (0..12) + let values = (0..11) .map(|index| { let offset = VULKAN_POINT_LIGHT_HEADER_BYTES + index * std::mem::size_of::<f32>(); f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) @@ -514,7 +541,11 @@ fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { .collect::<Vec<_>>(); assert_eq!( values, - vec![1.0, 2.0, 3.0, 7.0, 4.0, 5.0, 6.0, 1.0, 8.0, 9.0, 10.0, 1.0] + vec![1.0, 2.0, 3.0, 7.0, 4.0, 5.0, 6.0, 1.0, 8.0, 9.0, 10.0] + ); + assert_eq!( + u32::from_ne_bytes(bytes[60..64].try_into().unwrap()), + 0x1234_5678 ); let many = vec![VulkanPointLight::default(); 33]; |
