aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/packet_distance.rs52
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/reflection.rs6
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs3
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs109
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs12
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs37
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];