aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs27
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs1
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/reflection.rs348
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs8
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs503
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs11
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs69
7 files changed, 789 insertions, 178 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
index 826e89a..d813b91 100644
--- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs
+++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
@@ -866,6 +866,7 @@ fn append_node_fallback_batch(
batch_flags: batch.batch_flags,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
Ok(())
@@ -1226,6 +1227,7 @@ fn static_terrain_draw_ranges(
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
let material_index = first_face.material_tag;
let terrain_base_only = first_face.flags.0 & 0x400 == 0;
+ let terrain_reflection = first_face.flags.0 & 0x20000 != 0;
let mut end_face = start_face + 1;
while end_face < lod0.len() {
let next_face = terrain
@@ -1234,6 +1236,10 @@ fn static_terrain_draw_ranges(
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
if next_face.material_tag != material_index
|| (next_face.flags.0 & 0x400 == 0) != terrain_base_only
+ || (next_face.flags.0 & 0x20000 != 0) != terrain_reflection
+ || (terrain_reflection
+ && terrain.positions[usize::from(first_face.vertices[0])][2]
+ != terrain.positions[usize::from(next_face.vertices[0])][2])
{
break;
}
@@ -1253,6 +1259,7 @@ fn static_terrain_draw_ranges(
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only,
+ terrain_reflection,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
start_face = end_face;
@@ -1299,6 +1306,7 @@ fn static_model_indices_and_ranges(
batch_flags: batch.batch_flags,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
});
}
@@ -1506,6 +1514,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
}]
);
@@ -2036,6 +2045,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference:
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
},
@@ -2047,6 +2057,7 @@ mod tests {
batch_flags: 0,
pipeline_state: world_pipeline_state(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference:
VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
},
@@ -2298,5 +2309,21 @@ mod tests {
assert!(ranges.iter().all(|range| {
range.alpha_test_reference == VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE
}));
+ terrain.faces = vec![
+ terrain_face_with_tag_and_flags(0xff04, 0x400, [0, 1, 2]),
+ terrain_face_with_tag_and_flags(0xff04, 0x20400, [0, 1, 2]),
+ terrain_face_with_tag_and_flags(0xff04, 0x20400, [0, 1, 2]),
+ terrain_face_with_tag_and_flags(0xff04, 0x20400, [3, 4, 5]),
+ ];
+ terrain.positions.extend([[0.0, 0.0, 10.0]; 3]);
+ set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]);
+ let assignments = static_terrain_lod0_assignments(&terrain).unwrap();
+ let ranges = static_terrain_draw_ranges(&terrain, &assignments).unwrap();
+ assert_eq!(ranges.len(), 3);
+ assert!(!ranges[0].terrain_reflection);
+ assert!(ranges[1].terrain_reflection);
+ assert_eq!(ranges[1].index_count, 6);
+ assert!(ranges[2].terrain_reflection);
+ assert_eq!(ranges[2].index_count, 3);
}
}
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index b98fa0d..030b8ac 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -11,6 +11,7 @@
mod asset_mesh;
mod capabilities;
mod instance;
+mod reflection;
mod resources;
mod runtime;
mod smoke;
diff --git a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs
new file mode 100644
index 0000000..eb4025e
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs
@@ -0,0 +1,348 @@
+#![allow(unsafe_code)]
+
+use super::{
+ VulkanAllocatedImage, VulkanDepthAttachment, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
+ VulkanSmokeRendererError,
+};
+use ash::vk;
+
+pub(super) fn reflected_pipeline_state(
+ mut state: fparkan_render::LegacyPipelineState,
+) -> fparkan_render::LegacyPipelineState {
+ use fparkan_render::LegacyCullMode;
+ state.cull = match state.cull {
+ LegacyCullMode::Disabled => LegacyCullMode::Disabled,
+ LegacyCullMode::BackFace => LegacyCullMode::FrontFace,
+ LegacyCullMode::FrontFace => LegacyCullMode::BackFace,
+ };
+ state
+}
+
+pub(super) fn mesh_reflection_planes(mesh: &super::VulkanStaticMesh) -> Vec<Option<f32>> {
+ mesh.draw_ranges
+ .iter()
+ .map(|range| {
+ if !range.terrain_reflection || range.index_count == 0 {
+ return None;
+ }
+ let indices = &mesh.indices[range.first_index as usize
+ ..range.first_index as usize + range.index_count as usize];
+ let height = mesh.vertices[indices[0] as usize].position[2];
+ indices
+ .iter()
+ .all(|&index| (mesh.vertices[index as usize].position[2] - height).abs() <= 1.0e-4)
+ .then_some(height)
+ })
+ .collect()
+}
+
+/// Owned before pipeline creation; a dummy set prevents attachment feedback in the mirror pass.
+pub(super) struct ReflectionResources {
+ device: ash::Device,
+ pub layout: vk::DescriptorSetLayout,
+ pool: vk::DescriptorPool,
+ sampler: vk::Sampler,
+ pub sets: [vk::DescriptorSet; 2],
+ dummy: Option<VulkanAllocatedImage>,
+ pub color: Option<VulkanAllocatedImage>,
+ depth: Option<VulkanDepthAttachment>,
+ pub framebuffer: vk::Framebuffer,
+}
+
+impl ReflectionResources {
+ pub fn new(
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ ) -> Result<Self, VulkanSmokeRendererError> {
+ let mut owned = Self {
+ device: device.device().clone(),
+ layout: vk::DescriptorSetLayout::null(),
+ pool: vk::DescriptorPool::null(),
+ sampler: vk::Sampler::null(),
+ sets: [vk::DescriptorSet::null(); 2],
+ dummy: None,
+ color: None,
+ depth: None,
+ framebuffer: vk::Framebuffer::null(),
+ };
+ let binding = vk::DescriptorSetLayoutBinding::default()
+ .binding(0)
+ .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
+ .descriptor_count(1)
+ .stage_flags(vk::ShaderStageFlags::FRAGMENT);
+ let layout_info =
+ vk::DescriptorSetLayoutCreateInfo::default().bindings(std::slice::from_ref(&binding));
+ // SAFETY: The device is live; owned rolls back every handle on later failure.
+ owned.layout = unsafe {
+ owned
+ .device
+ .create_descriptor_set_layout(&layout_info, None)
+ }
+ .map_err(|result| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateDescriptorSetLayout(reflection)",
+ result,
+ })?;
+ let size = vk::DescriptorPoolSize::default()
+ .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
+ .descriptor_count(2);
+ let pool_info = vk::DescriptorPoolCreateInfo::default()
+ .max_sets(2)
+ .pool_sizes(std::slice::from_ref(&size));
+ // SAFETY: The pool owns only the two reflection descriptor sets below.
+ owned.pool =
+ unsafe { owned.device.create_descriptor_pool(&pool_info, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateDescriptorPool(reflection)",
+ result,
+ }
+ })?;
+ let layouts = [owned.layout; 2];
+ let allocation = vk::DescriptorSetAllocateInfo::default()
+ .descriptor_pool(owned.pool)
+ .set_layouts(&layouts);
+ // SAFETY: The pool has capacity for these two live layouts.
+ let sets =
+ unsafe { owned.device.allocate_descriptor_sets(&allocation) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkAllocateDescriptorSets(reflection)",
+ result,
+ }
+ })?;
+ owned.sets.copy_from_slice(&sets);
+ let sampler = vk::SamplerCreateInfo::default()
+ .mag_filter(vk::Filter::LINEAR)
+ .min_filter(vk::Filter::LINEAR)
+ .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
+ .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
+ .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE);
+ // SAFETY: No optional device feature is used by this sampler.
+ owned.sampler =
+ unsafe { owned.device.create_sampler(&sampler, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateSampler(reflection)",
+ result,
+ }
+ })?;
+ // Material images can be replaced by animation, so the dummy has its own lifetime.
+ owned.dummy = Some(super::create_static_texture_image(
+ instance,
+ device,
+ command_pool,
+ &super::VulkanStaticTexture {
+ width: 1,
+ height: 1,
+ rgba8: vec![255; 4],
+ mip_levels: Vec::new(),
+ },
+ )?);
+ for set in owned.sets {
+ owned.write_image(set, owned.dummy.as_ref().expect("dummy image exists").view);
+ }
+ Ok(owned)
+ }
+
+ pub fn create_target(
+ &mut self,
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ extent: (u32, u32),
+ format: i32,
+ depth_request: fparkan_platform::DepthStencilSupport,
+ render_pass: vk::RenderPass,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ let info = vk::ImageCreateInfo::default()
+ .image_type(vk::ImageType::TYPE_2D)
+ .format(vk::Format::from_raw(format))
+ .extent(vk::Extent3D {
+ width: extent.0,
+ height: extent.1,
+ depth: 1,
+ })
+ .mip_levels(1)
+ .array_layers(1)
+ .samples(vk::SampleCountFlags::TYPE_1)
+ .tiling(vk::ImageTiling::OPTIMAL)
+ .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.
+ let image = unsafe { self.device.create_image(&info, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateImage(reflection)",
+ result,
+ }
+ })?;
+ self.color = Some(VulkanAllocatedImage {
+ image,
+ memory: vk::DeviceMemory::null(),
+ view: vk::ImageView::null(),
+ width: extent.0,
+ height: extent.1,
+ mip_levels: 1,
+ });
+ // SAFETY: The image belongs to this device and has not been bound yet.
+ let requirements = unsafe { self.device.get_image_memory_requirements(image) };
+ let memory_type_index = super::resources::find_memory_type(
+ instance,
+ device.physical_device(),
+ requirements.memory_type_bits,
+ vk::MemoryPropertyFlags::DEVICE_LOCAL,
+ )
+ .ok_or(VulkanSmokeRendererError::MissingMemoryType {
+ context: "reflection target",
+ })?;
+ let allocation = vk::MemoryAllocateInfo::default()
+ .allocation_size(requirements.size)
+ .memory_type_index(memory_type_index);
+ // SAFETY: The allocation follows this image's memory requirements.
+ let memory =
+ unsafe { self.device.allocate_memory(&allocation, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkAllocateMemory(reflection)",
+ result,
+ }
+ })?;
+ self.color.as_mut().expect("color image exists").memory = memory;
+ // SAFETY: The allocation is compatible with the image and offset zero.
+ unsafe { self.device.bind_image_memory(image, memory, 0) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkBindImageMemory(reflection)",
+ result,
+ }
+ })?;
+ let view_info = vk::ImageViewCreateInfo::default()
+ .image(image)
+ .view_type(vk::ImageViewType::TYPE_2D)
+ .format(vk::Format::from_raw(format))
+ .subresource_range(super::color_subresource_range(1));
+ // SAFETY: The image is bound and the view covers its single color subresource.
+ let view =
+ unsafe { self.device.create_image_view(&view_info, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateImageView(reflection)",
+ result,
+ }
+ })?;
+ self.color.as_mut().expect("color image exists").view = view;
+ self.depth = Some(super::create_depth_attachment(
+ instance,
+ device,
+ extent,
+ depth_request,
+ )?);
+ let attachments = [view, self.depth.as_ref().expect("depth exists").image.view];
+ let info = vk::FramebufferCreateInfo::default()
+ .render_pass(render_pass)
+ .attachments(&attachments)
+ .width(extent.0)
+ .height(extent.1)
+ .layers(1);
+ // SAFETY: The target formats and extent match the shared render pass.
+ self.framebuffer =
+ unsafe { self.device.create_framebuffer(&info, None) }.map_err(|result| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateFramebuffer(reflection)",
+ result,
+ }
+ })?;
+ self.write_image(self.sets[1], view);
+ Ok(())
+ }
+
+ fn write_image(&self, set: vk::DescriptorSet, view: vk::ImageView) {
+ let image = vk::DescriptorImageInfo::default()
+ .sampler(self.sampler)
+ .image_view(view)
+ .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
+ let write = vk::WriteDescriptorSet::default()
+ .dst_set(set)
+ .dst_binding(0)
+ .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
+ .image_info(std::slice::from_ref(&image));
+ // SAFETY: Sets are initialized before submission; the dummy remains valid for every pass.
+ unsafe { self.device.update_descriptor_sets(&[write], &[]) };
+ }
+}
+
+impl Drop for ReflectionResources {
+ fn drop(&mut self) {
+ // SAFETY: The parent waits for submissions and drops this owner before the render pass/device.
+ // Null handles also cover partial construction, including failures before memory binding.
+ unsafe {
+ self.device.destroy_framebuffer(self.framebuffer, None);
+ self.device.destroy_descriptor_pool(self.pool, None);
+ self.device.destroy_sampler(self.sampler, None);
+ self.device.destroy_descriptor_set_layout(self.layout, None);
+ for image in self
+ .color
+ .iter()
+ .chain(self.depth.iter().map(|depth| &depth.image))
+ .chain(self.dummy.iter())
+ {
+ self.device.destroy_image_view(image.view, None);
+ self.device.destroy_image(image.image, None);
+ self.device.free_memory(image.memory, None);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
+
+ #[test]
+ fn reflection_requires_a_flat_flagged_surface_and_reverses_only_culling() {
+ let mut mesh = VulkanStaticMesh {
+ vertices: vec![
+ VulkanStaticVertex {
+ position: [0.0, 0.0, 150.0],
+ color: [1.0; 3],
+ normal: [0.0, 0.0, 1.0],
+ uv: [0.0; 2],
+ detail_uv: [0.0; 2],
+ overlay_alpha: 0.0,
+ };
+ 3
+ ],
+ indices: vec![0, 1, 2],
+ draw_ranges: vec![VulkanStaticDrawRange {
+ first_index: 0,
+ index_count: 3,
+ material_index: 0,
+ lightmap_index: 255,
+ batch_flags: 0,
+ terrain_base_only: false,
+ terrain_reflection: true,
+ pipeline_state: fparkan_render::LegacyPipelineState::default(),
+ alpha_test_reference: 1,
+ }],
+ };
+ assert_eq!(mesh_reflection_planes(&mesh), vec![Some(150.0)]);
+ mesh.vertices[2].position[2] = 151.0;
+ assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
+ mesh.vertices[2].position[2] = 150.0;
+ mesh.draw_ranges[0].terrain_reflection = false;
+ assert_eq!(mesh_reflection_planes(&mesh), vec![None]);
+ for cull in [
+ fparkan_render::LegacyCullMode::Disabled,
+ fparkan_render::LegacyCullMode::BackFace,
+ fparkan_render::LegacyCullMode::FrontFace,
+ ] {
+ let mut state = mesh.draw_ranges[0].pipeline_state;
+ state.cull = cull;
+ state.alpha_test = true;
+ state.depth_bias = fparkan_render::LegacyDepthBias::ProjectedDecal;
+ assert_eq!(
+ reflected_pipeline_state(reflected_pipeline_state(state)),
+ state
+ );
+ let reflected = reflected_pipeline_state(state);
+ assert_eq!(reflected.depth, state.depth);
+ assert_eq!(reflected.blend, state.blend);
+ assert_eq!(reflected.alpha_test, state.alpha_test);
+ }
+ }
+}
diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
index fe71d12..498a472 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
@@ -528,7 +528,7 @@ pub(super) fn create_static_texture_image(
})
}
-/// Allocates two aligned camera slots used by the dynamic frame UBO.
+/// Allocates a main and reflected view slot for each in-flight frame.
pub(super) fn create_frame_uniform_buffer(
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
@@ -545,7 +545,7 @@ pub(super) fn create_frame_uniform_buffer(
)? / alignment
* alignment;
let size = stride
- .checked_mul(super::VULKAN_FRAME_SLOT_COUNT as u64)
+ .checked_mul(2 * super::VULKAN_FRAME_SLOT_COUNT as u64)
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
context: "frame uniform buffer size overflow",
})?;
@@ -888,7 +888,7 @@ pub(super) fn write_frame_uniform(
.ok_or(VulkanSmokeRendererError::InvariantViolation {
context: "frame uniform slot offset",
})?;
- // SAFETY: The offset is one of the two allocated aligned slots and the
+ // SAFETY: The caller supplies an allocated, fence-safe main or reflection slot; the
// mapped range is exactly one complete frame block.
let mapped = unsafe {
device.device().map_memory(
@@ -1385,7 +1385,7 @@ fn create_host_visible_buffer(
Ok(VulkanAllocatedBuffer { buffer, memory })
}
-fn find_memory_type(
+pub(super) fn find_memory_type(
instance: &VulkanInstanceProbe,
physical_device: vk::PhysicalDevice,
type_bits: u32,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index 1b50914..5968a47 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -357,6 +357,28 @@ impl VulkanSmokeRenderer {
.map(|material| material.material_index)
.collect()
};
+ let reflection_planes = super::reflection::mesh_reflection_planes(&create_info.mesh);
+ let unsupported_nonplanar = create_info
+ .mesh
+ .draw_ranges
+ .iter()
+ .zip(&reflection_planes)
+ .filter(|(range, plane)| range.terrain_reflection && plane.is_none())
+ .count();
+ let mut plane_heights = reflection_planes
+ .iter()
+ .flatten()
+ .map(|z| z.to_bits())
+ .collect::<Vec<_>>();
+ plane_heights.sort_unstable();
+ plane_heights.dedup();
+ if !plane_heights.is_empty() {
+ eprintln!("render_coverage planar_reflection_embm_unsupported={} (flat reflection; native shifted camera and wave distortion pending)", 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 {
+ eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)");
+ }
let mut renderer = Self {
instance: Some(instance),
validation,
@@ -379,6 +401,7 @@ impl VulkanSmokeRenderer {
.map(|range| range.index_count)
.collect(),
draw_ranges: create_info.mesh.draw_ranges.clone(),
+ reflection_planes,
draw_texture_indices,
draw_order: (0..create_info.mesh.draw_ranges.len()).collect(),
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
@@ -521,6 +544,22 @@ impl VulkanSmokeRenderer {
self.frame_uniforms
}
+ fn active_reflection_plane(&self) -> Option<f32> {
+ // shortcut: one target serves the nearest active water height; add per-plane targets
+ // when accepting maps whose visible water surfaces have different heights.
+ self.draw_ranges
+ .iter()
+ .zip(&self.reflection_planes)
+ .filter(|(range, _)| range.index_count != 0)
+ .filter_map(|(_, plane)| *plane)
+ .filter(|&z| z < self.frame_uniforms.camera_position[2])
+ .min_by(|a, b| {
+ (self.frame_uniforms.camera_position[2] - a)
+ .abs()
+ .total_cmp(&(self.frame_uniforms.camera_position[2] - b).abs())
+ })
+ }
+
/// Replaces the camera for subsequent frames without recreating GPU resources.
///
/// # Errors
@@ -1516,6 +1555,27 @@ impl VulkanSmokeRenderer {
}
self.images_in_flight[image_index_usize] = in_flight_fence;
let frame_bytes = self.frame_uniforms.to_ne_bytes();
+ let reflection_bytes = self
+ .active_reflection_plane()
+ .map(|height| {
+ let mut frame = self.frame_uniforms;
+ frame.clip_from_world =
+ fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height)
+ .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 {
+ context: "non-finite reflected camera position",
+ });
+ }
+ let mut bytes = frame.to_ne_bytes();
+ let length = bytes.len();
+ bytes[length - 4..].copy_from_slice(&height.to_ne_bytes());
+ Ok(bytes)
+ })
+ .transpose()?;
{
let explicit_point_lights = self.point_lights_explicit;
let legacy_point_light = self.frame_uniforms.point_light;
@@ -1541,9 +1601,18 @@ impl VulkanSmokeRenderer {
device,
&resources.frame_uniform_buffer,
resources.frame_uniform_stride,
- self.current_frame,
+ self.current_frame * 2,
&frame_bytes,
)?;
+ if let Some(bytes) = &reflection_bytes {
+ write_frame_uniform(
+ device,
+ &resources.frame_uniform_buffer,
+ resources.frame_uniform_stride,
+ self.current_frame * 2 + 1,
+ bytes,
+ )?;
+ }
write_point_light_buffer(
device,
&resources.point_light_buffer,
@@ -1686,6 +1755,9 @@ impl VulkanSmokeRenderer {
self.textures_ref()?,
&self.texture_slot_indices,
&self.draw_range_base_pipeline_states,
+ self.draw_ranges
+ .iter()
+ .any(|range| range.terrain_reflection),
self.depth_request,
reuse_command_pool,
)?,
@@ -1741,6 +1813,7 @@ impl VulkanSmokeRenderer {
self.rebuild_draw_order();
self.draw_order_dirty = false;
}
+ let reflection_plane = self.active_reflection_plane();
let device = self.device_ref()?;
let swapchain = self.swapchain_ref()?;
let resources = self.resources_ref()?;
@@ -1787,185 +1860,269 @@ impl VulkanSmokeRenderer {
height: swapchain.report.plan.extent.1,
},
};
- let render_pass_info = vk::RenderPassBeginInfo::default()
- .render_pass(resources.render_pass)
- .framebuffer(resources.framebuffers[image_index])
- .render_area(render_area)
- .clear_values(&clear_values);
- // SAFETY: All commands target live frame resources owned by this renderer.
- unsafe {
- device.device().cmd_begin_render_pass(
- command_buffer,
- &render_pass_info,
- vk::SubpassContents::INLINE,
- );
- let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
- let offsets = [0_u64];
- device
- .device()
- .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
- device.device().cmd_bind_index_buffer(
- command_buffer,
- self.index_buffer_ref()?.buffer,
- 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 {
- context: "stable draw order range handle",
- },
- )?;
- let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "stable draw texture range handle",
- },
- )?;
- let pipeline = resources.pipelines.get(&range.pipeline_key()).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static draw pipeline variant",
- },
- )?;
- device.device().cmd_bind_pipeline(
+ for reflected in [true, false] {
+ if reflected && reflection_plane.is_none() {
+ continue;
+ }
+ let framebuffer = if reflected {
+ resources.reflection.framebuffer
+ } else {
+ resources.framebuffers[image_index]
+ };
+ let render_pass_info = vk::RenderPassBeginInfo::default()
+ .render_pass(resources.render_pass)
+ .framebuffer(framebuffer)
+ .render_area(render_area)
+ .clear_values(&clear_values);
+ // SAFETY: All commands target live frame resources owned by this renderer.
+ unsafe {
+ device.device().cmd_begin_render_pass(
command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- *pipeline,
+ &render_pass_info,
+ vk::SubpassContents::INLINE,
);
- let frame_offset = u32::try_from(
- resources
- .frame_uniform_stride
- .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let material_offset = u32::try_from(
- resources
- .material_specular_stride
- .saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let point_light_offset = u32::try_from(
- resources
- .point_light_stride
- .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material descriptor set",
- },
- )?;
- device.device().cmd_bind_descriptor_sets(
+ let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
+ let offsets = [0_u64];
+ device.device().cmd_bind_vertex_buffers(
command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- resources.pipeline_layout,
0,
- std::slice::from_ref(descriptor_set),
- &[frame_offset, material_offset, point_light_offset],
+ &vertex_buffers,
+ &offsets,
);
- let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
- device.device().cmd_push_constants(
+ device.device().cmd_bind_index_buffer(
command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ self.index_buffer_ref()?.buffer,
0,
- &alpha_cutoff,
- );
- let material_alpha = self.material_alphas.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material alpha",
- },
- )?;
- 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,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 4,
- &material_alphas,
- );
- let material_lighting = self.material_lighting.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material lighting",
- },
- )?;
- let material_lighting = material_lighting
- .iter()
- .flatten()
- .flat_map(|value| value.to_ne_bytes())
- .collect::<Vec<_>>();
- device.device().cmd_push_constants(
- command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 12,
- &material_lighting,
- );
- let material_uv_transforms = self.material_uv_transforms.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material UV transforms",
- },
- )?;
- let material_uv_transforms = material_uv_transforms
- .iter()
- .flatten()
- .flat_map(|value| value.to_ne_bytes())
- .collect::<Vec<_>>();
- device.device().cmd_push_constants(
- command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 60,
- &material_uv_transforms,
+ vk::IndexType::UINT32,
);
- let material_mode = self.material_unlit.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material unlit flag",
- },
- )?;
- let material_mode = if range.terrain_base_only {
- ((*material_mode).round() as i32 | 16) as f32
- } 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(
+ device.device().cmd_bind_descriptor_sets(
command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 124,
- &material_mode.concat(),
- );
- device.device().cmd_draw_indexed(
- command_buffer,
- range.index_count,
1,
- range.first_index,
- 0,
- 0,
+ &[resources.reflection.sets
+ [usize::from(!reflected && reflection_plane.is_some())]],
+ &[],
);
+ let mut viewport_depth = None;
+ for &range_index in &self.draw_order {
+ let range = self.draw_ranges.get(range_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "stable draw order range handle",
+ },
+ )?;
+ let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "stable draw texture range handle",
+ },
+ )?;
+ let mode = self.material_unlit[texture_index].round() as i32;
+ if reflected && (range.terrain_reflection || mode & 8 != 0) {
+ continue;
+ }
+ let state = if reflected {
+ super::reflection::reflected_pipeline_state(range.pipeline_state)
+ } else {
+ range.pipeline_state
+ };
+ let pipeline = resources.pipelines.get(&state.into()).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static draw pipeline variant",
+ },
+ )?;
+ device.device().cmd_bind_pipeline(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ *pipeline,
+ );
+ let frame_offset = u32::try_from(
+ resources.frame_uniform_stride.saturating_mul(
+ u64::try_from(self.current_frame * 2 + usize::from(reflected))
+ .unwrap_or(u64::MAX),
+ ),
+ )
+ .unwrap_or(u32::MAX);
+ let material_offset = u32::try_from(
+ resources
+ .material_specular_stride
+ .saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
+ )
+ .unwrap_or(u32::MAX);
+ let point_light_offset = u32::try_from(
+ resources
+ .point_light_stride
+ .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
+ )
+ .unwrap_or(u32::MAX);
+ let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material descriptor set",
+ },
+ )?;
+ device.device().cmd_bind_descriptor_sets(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ resources.pipeline_layout,
+ 0,
+ std::slice::from_ref(descriptor_set),
+ &[frame_offset, material_offset, point_light_offset],
+ );
+ let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 0,
+ &alpha_cutoff,
+ );
+ let material_alpha = self.material_alphas.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material alpha",
+ },
+ )?;
+ 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,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 4,
+ &material_alphas,
+ );
+ let material_lighting = self.material_lighting.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material lighting",
+ },
+ )?;
+ let material_lighting = material_lighting
+ .iter()
+ .flatten()
+ .flat_map(|value| value.to_ne_bytes())
+ .collect::<Vec<_>>();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 12,
+ &material_lighting,
+ );
+ let material_uv_transforms = self
+ .material_uv_transforms
+ .get(texture_index)
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "static material UV transforms",
+ })?;
+ let material_uv_transforms = material_uv_transforms
+ .iter()
+ .flatten()
+ .flat_map(|value| value.to_ne_bytes())
+ .collect::<Vec<_>>();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 60,
+ &material_uv_transforms,
+ );
+ let material_mode = self.material_unlit.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material unlit flag",
+ },
+ )?;
+ let material_mode = if range.terrain_base_only {
+ ((*material_mode).round() as i32 | 16) as f32
+ } else {
+ *material_mode
+ };
+ let material_mode = (material_mode.round() as i32
+ | if reflected { 32 } else { 0 }
+ | if !reflected
+ && reflection_plane.is_some()
+ && self.reflection_planes[range_index] == reflection_plane
+ {
+ 64
+ } else {
+ 0
+ }) as f32;
+ 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,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 124,
+ &material_mode.concat(),
+ );
+ device.device().cmd_draw_indexed(
+ command_buffer,
+ range.index_count,
+ 1,
+ range.first_index,
+ 0,
+ 0,
+ );
+ }
+ device.device().cmd_end_render_pass(command_buffer);
+ if reflected {
+ let image = resources.reflection.color.as_ref().ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "reflection target",
+ },
+ )?;
+ let barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .dst_access_mask(vk::AccessFlags::SHADER_READ)
+ .image(image.image)
+ .subresource_range(super::color_subresource_range(1));
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::FRAGMENT_SHADER,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[barrier],
+ );
+ }
}
- device.device().cmd_end_render_pass(command_buffer);
+ }
+ // SAFETY: The main pass has completed its writes to this acquired swapchain image.
+ unsafe {
+ let barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .image(resources.images[image_index])
+ .subresource_range(super::color_subresource_range(1));
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::BOTTOM_OF_PIPE,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[barrier],
+ );
if self.readback_enabled {
if let (Some(image), Some(readback)) = (
resources.images.get(image_index),
@@ -2028,7 +2185,7 @@ impl VulkanSmokeRenderer {
}
}
- // SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
+ // SAFETY: Every render pass and attachment transition has been recorded in this buffer.
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkEndCommandBuffer",
@@ -2720,6 +2877,7 @@ mod tests {
batch_flags: 0x108,
pipeline_state: base_state,
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 17,
},
VulkanStaticDrawRange {
@@ -2730,6 +2888,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -2886,6 +3045,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
}];
let mut capacities = [6];
@@ -3072,6 +3232,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3096,6 +3257,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -3106,6 +3268,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -3116,6 +3279,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -3140,6 +3304,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3233,6 +3398,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3291,6 +3457,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index 5d6f12f..bc8aedd 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -437,6 +437,8 @@ pub struct VulkanStaticDrawRange {
pub batch_flags: u16,
/// Terrain ranges that use the native lit base-only path (face flag 0x400 clear).
pub terrain_base_only: bool,
+ /// Terrain face flag 0x20000 requests a water reflection surface.
+ pub terrain_reflection: bool,
/// Backend-neutral fixed-function state for this source range.
pub pipeline_state: LegacyPipelineState,
/// Alpha-test reference in legacy 0..=255 units; dynamic material data.
@@ -771,6 +773,7 @@ impl VulkanStaticMesh {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
}],
}
@@ -1032,6 +1035,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
}],
};
@@ -1063,6 +1067,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
VulkanStaticDrawRange {
@@ -1073,6 +1078,7 @@ mod static_mesh_tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -1191,6 +1197,7 @@ mod static_mesh_tests {
let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0];
let disabled = VulkanStaticDrawRange {
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 200,
..base
};
@@ -1200,6 +1207,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 128,
..base
};
@@ -1211,6 +1219,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
..base
};
@@ -1221,6 +1230,7 @@ mod static_mesh_tests {
..LegacyPipelineState::default()
},
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE,
..base
};
@@ -1501,6 +1511,7 @@ pub struct VulkanSmokeRenderer {
pub(super) index_capacity: usize,
pub(super) draw_range_capacities: Vec<u32>,
pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
+ pub(super) reflection_planes: Vec<Option<f32>>,
pub(super) draw_texture_indices: Vec<usize>,
/// Stable range handles used when recording the native pass order.
pub(super) draw_order: Vec<usize>,
diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
index a44f79d..4365cd2 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs
@@ -18,6 +18,7 @@ use super::{
};
pub(super) struct VulkanSwapchainResources {
+ pub(super) reflection: super::reflection::ReflectionResources,
pub(super) image_views: Vec<vk::ImageView>,
pub(super) images: Vec<vk::Image>,
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
@@ -146,6 +147,7 @@ pub(super) fn ensure_point_light_capacity(
}
struct PartialSwapchainResources {
+ reflection: Option<super::reflection::ReflectionResources>,
image_views: Vec<vk::ImageView>,
readback_buffers: Vec<VulkanAllocatedBuffer>,
depth_attachments: Vec<VulkanDepthAttachment>,
@@ -180,6 +182,7 @@ pub(super) fn create_swapchain_resources(
textures: &[VulkanAllocatedImage],
texture_slot_indices: &[usize],
base_pipeline_states: &[LegacyPipelineState],
+ reflection_enabled: bool,
depth_request: DepthStencilSupport,
reuse_command_pool: bool,
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
@@ -194,6 +197,7 @@ pub(super) fn create_swapchain_resources(
result: error,
})?;
let mut partial = PartialSwapchainResources {
+ reflection: None,
image_views: create_swapchain_image_views(
device,
&images,
@@ -245,6 +249,14 @@ pub(super) fn create_swapchain_resources(
};
partial.frame_uniform_stride = frame_uniform_stride;
partial.frame_uniform_buffer = Some(frame_uniform_buffer);
+ partial.reflection =
+ match super::reflection::ReflectionResources::new(instance, device, command_pool) {
+ Ok(reflection) => Some(reflection),
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ };
let material_count = texture_slot_indices.len() / 4;
let (material_specular_buffer, material_specular_stride) =
match create_material_specular_buffer(instance, device, material_count) {
@@ -288,6 +300,12 @@ pub(super) fn create_swapchain_resources(
textures,
texture_slot_indices,
base_pipeline_states,
+ partial
+ .reflection
+ .as_ref()
+ .expect("reflection descriptors exist")
+ .layout,
+ reflection_enabled,
depth_request,
partial
.frame_uniform_buffer
@@ -319,6 +337,24 @@ pub(super) fn create_swapchain_resources(
partial.descriptor_pool = Some(descriptor_pool);
partial.sampler = Some(sampler);
partial.pipelines = pipelines;
+ if reflection_enabled {
+ if let Err(error) = partial
+ .reflection
+ .as_mut()
+ .expect("reflection descriptors exist")
+ .create_target(
+ instance,
+ device,
+ swapchain.report.plan.extent,
+ swapchain.report.plan.format.format,
+ depth_request,
+ render_pass,
+ )
+ {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ }
for _ in 1..images.len() {
match create_depth_attachment(
instance,
@@ -394,6 +430,10 @@ pub(super) fn create_swapchain_resources(
});
}
Ok(VulkanSwapchainResources {
+ reflection: partial
+ .reflection
+ .take()
+ .expect("reflection descriptors exist"),
image_views: partial.image_views,
images,
readback_buffers: partial.readback_buffers,
@@ -457,6 +497,8 @@ fn create_swapchain_pipeline_bundle(
textures: &[VulkanAllocatedImage],
texture_slot_indices: &[usize],
base_pipeline_states: &[LegacyPipelineState],
+ reflection_layout: vk::DescriptorSetLayout,
+ reflection_enabled: bool,
depth_request: DepthStencilSupport,
frame_uniform_buffer: &VulkanAllocatedBuffer,
material_specular_buffer: &VulkanAllocatedBuffer,
@@ -498,8 +540,8 @@ fn create_swapchain_pipeline_bundle(
unsafe { device.device().destroy_render_pass(render_pass, None) };
destroy_depth_attachment(device, &depth_attachment);
})?;
- let pipeline_layout =
- create_pipeline_layout(device, descriptor_set_layout).inspect_err(|_| {
+ let pipeline_layout = create_pipeline_layout(device, descriptor_set_layout, reflection_layout)
+ .inspect_err(|_| {
// SAFETY: The descriptor resources and render pass were created above and are rolled back on this device.
unsafe {
device.device().destroy_sampler(sampler, None);
@@ -520,6 +562,7 @@ fn create_swapchain_pipeline_bundle(
pipeline_layout,
extent,
base_pipeline_states,
+ reflection_enabled,
)
.inspect_err(|_| {
// SAFETY: These objects were created above on this live logical device and are destroyed on setup failure.
@@ -636,7 +679,7 @@ fn create_render_pass(
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.initial_layout(vk::ImageLayout::UNDEFINED)
- .final_layout(vk::ImageLayout::PRESENT_SRC_KHR);
+ .final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
let color_attachment_ref = vk::AttachmentReference::default()
.attachment(0)
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
@@ -661,7 +704,8 @@ fn create_render_pass(
.src_subpass(vk::SUBPASS_EXTERNAL)
.dst_subpass(0)
.src_stage_mask(
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
+ vk::PipelineStageFlags::FRAGMENT_SHADER
+ | vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
)
@@ -670,7 +714,11 @@ fn create_render_pass(
| vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
)
- .src_access_mask(vk::AccessFlags::empty())
+ .src_access_mask(
+ vk::AccessFlags::SHADER_READ
+ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE
+ | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
+ )
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
| vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
@@ -694,8 +742,9 @@ fn create_render_pass(
fn create_pipeline_layout(
device: &VulkanLogicalDeviceProbe,
descriptor_set_layout: vk::DescriptorSetLayout,
+ reflection_layout: vk::DescriptorSetLayout,
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
- let set_layouts = [descriptor_set_layout];
+ let set_layouts = [descriptor_set_layout, reflection_layout];
// Material data occupies one 128-byte push-constant allocation shared by
// both stages; the vertex stage also reads its specular pair from the
// dynamic storage buffer at descriptor binding 5. Camera data is supplied
@@ -960,12 +1009,18 @@ fn create_graphics_pipeline_cache(
pipeline_layout: vk::PipelineLayout,
extent: (u32, u32),
base_pipeline_states: &[LegacyPipelineState],
+ reflection_enabled: bool,
) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> {
let mut pipelines = BTreeMap::new();
for state in base_pipeline_states
.iter()
.copied()
.flat_map(super::smoke::pipeline_variants)
+ .flat_map(|state| {
+ std::iter::once(state).chain(
+ reflection_enabled.then_some(super::reflection::reflected_pipeline_state(state)),
+ )
+ })
{
let key = PipelineKey::from(state);
if pipelines.contains_key(&key) {
@@ -1238,6 +1293,7 @@ pub(super) fn destroy_swapchain_resources(
command_pool: vk::CommandPool,
resources: VulkanSwapchainResources,
) {
+ drop(resources.reflection);
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
unsafe {
if !resources.command_buffers.is_empty() {
@@ -1303,6 +1359,7 @@ fn destroy_partial_swapchain_resources(
command_pool: vk::CommandPool,
partial: PartialSwapchainResources,
) {
+ drop(partial.reflection);
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
unsafe {
if !partial.command_buffers.is_empty() {