diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/reflection.rs | 452 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 8 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 541 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 58 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 73 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 26 |
6 files changed, 974 insertions, 184 deletions
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..1a9c9ae --- /dev/null +++ b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs @@ -0,0 +1,452 @@ +#![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() +} + +pub(super) fn mesh_reflection_bounds(mesh: &super::VulkanStaticMesh) -> Option<[f32; 4]> { + let mut bounds = [ + f32::INFINITY, + f32::INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + ]; + for range in mesh + .draw_ranges + .iter() + .filter(|range| range.terrain_reflection) + { + for &index in &mesh.indices + [range.first_index as usize..range.first_index as usize + range.index_count as usize] + { + let position = mesh.vertices[index as usize].position; + bounds[0] = bounds[0].min(position[0]); + bounds[1] = bounds[1].min(position[1]); + bounds[2] = bounds[2].max(position[0]); + bounds[3] = bounds[3].max(position[1]); + } + } + (bounds.iter().all(|v| v.is_finite()) && bounds[2] > bounds[0] && bounds[3] > bounds[1]) + .then_some(bounds) +} + +/// 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, + bump_sampler: vk::Sampler, + pub sets: [vk::DescriptorSet; 2], + dummy: Option<VulkanAllocatedImage>, + bump: 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, + enabled: bool, + ) -> Result<Self, VulkanSmokeRendererError> { + let mut owned = Self { + device: device.device().clone(), + layout: vk::DescriptorSetLayout::null(), + pool: vk::DescriptorPool::null(), + sampler: vk::Sampler::null(), + bump_sampler: vk::Sampler::null(), + sets: [vk::DescriptorSet::null(); 2], + dummy: None, + bump: None, + color: None, + depth: None, + framebuffer: vk::Framebuffer::null(), + }; + let bindings: Vec<_> = (0..2) + .map(|binding| { + vk::DescriptorSetLayoutBinding::default() + .binding(binding) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::FRAGMENT) + }) + .collect(); + let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); + // 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(4); + 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(), + }, + )?); + if enabled { + let sampler = sampler + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT); + // SAFETY: The immutable bump map uses supported linear filtering and a single mip level. + owned.bump_sampler = + unsafe { owned.device.create_sampler(&sampler, None) }.map_err(|result| { + VulkanSmokeRendererError::VulkanOperation { + context: "vkCreateSampler(EMBM)", + result, + } + })?; + let rgba8 = fparkan_render::water::legacy_embm_bump_map() + .into_iter() + .flat_map(|duv| { + [ + (i16::from(duv[0]) + 128) as u8, + (i16::from(duv[1]) + 128) as u8, + 0, + 255, + ] + }) + .collect(); + owned.bump = Some(super::create_static_texture_image( + instance, + device, + command_pool, + &super::VulkanStaticTexture { + width: fparkan_render::water::EMBM_BUMP_SIZE, + height: fparkan_render::water::EMBM_BUMP_SIZE, + rgba8, + 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 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)) + .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: 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)", + 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 bump = self + .bump + .as_ref() + .or(self.dummy.as_ref()) + .expect("dummy image exists"); + let images = [ + image, + vk::DescriptorImageInfo::default() + .sampler(if self.bump.is_some() { + self.bump_sampler + } else { + self.sampler + }) + .image_view(bump.view) + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL), + ]; + let writes: Vec<_> = images + .iter() + .enumerate() + .map(|(binding, image)| { + vk::WriteDescriptorSet::default() + .dst_set(set) + .dst_binding(binding as u32) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(std::slice::from_ref(image)) + }) + .collect(); + // SAFETY: Sets are initialized before submission; the dummy remains valid for every pass. + unsafe { self.device.update_descriptor_sets(&writes, &[]) }; + } +} + +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_sampler(self.bump_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()) + .chain(self.bump.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, + }], + }; + mesh.vertices[1].position = [10.0, 0.0, 150.0]; + mesh.vertices[2].position = [5.0, 20.0, 150.0]; + let mut unused = mesh.vertices[0]; + unused.position = [-10_000.0, -10_000.0, 150.0]; + mesh.vertices.push(unused); + assert_eq!(mesh_reflection_bounds(&mesh), Some([0.0, 0.0, 10.0, 20.0])); + 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]); + assert_eq!(mesh_reflection_bounds(&mesh), 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..a016645 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -357,6 +357,33 @@ 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_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 { + eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)"); + } + let mut frame_uniforms = super::VulkanFrameUniforms::from_camera(create_info.camera); + if let Some(bounds) = super::reflection::mesh_reflection_bounds(&create_info.mesh) { + frame_uniforms.embm_bounds = bounds; + eprintln!("render_reflection embm_bounds={bounds:?}"); + } let mut renderer = Self { instance: Some(instance), validation, @@ -379,6 +406,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()], @@ -401,7 +429,7 @@ impl VulkanSmokeRenderer { material_specular_dirty: true, readback_enabled: false, camera: create_info.camera, - frame_uniforms: super::VulkanFrameUniforms::from_camera(create_info.camera), + frame_uniforms, point_lights: Vec::new(), point_lights_explicit: false, frame_sync: Vec::new(), @@ -521,6 +549,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 +1560,34 @@ 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::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", + })?; + 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 offset = super::smoke_types::REFLECTION_CLIP_HEIGHT_OFFSET; + bytes[offset..offset + 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 +1613,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 +1767,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 +1825,16 @@ impl VulkanSmokeRenderer { self.rebuild_draw_order(); 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()?; @@ -1780,192 +1874,283 @@ 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, - }, - }; - 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( + 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: "static draw pipeline variant", + context: "active reflection without a color target", }, )?; - device.device().cmd_bind_pipeline( + (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 { + 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 | if shifted_reflection { 128 } else { 0 } + } else { + 0 + }) as f32; + 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 + .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 +2213,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 +2905,7 @@ mod tests { batch_flags: 0x108, pipeline_state: base_state, terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 17, }, VulkanStaticDrawRange { @@ -2730,6 +2916,7 @@ mod tests { batch_flags: 0, pipeline_state: LegacyPipelineState::default(), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, }, ]; @@ -2886,6 +3073,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 +3260,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 +3285,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 +3296,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 +3307,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 +3332,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 +3426,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 +3485,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..90fd7b4 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -192,7 +192,8 @@ impl Default for VulkanPointLight { /// every device. Four directional lights and one point light fit in a small /// dynamic UBO while the push-constant range remains available for material /// state. -pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 368; +pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 400; +pub(super) const REFLECTION_CLIP_HEIGHT_OFFSET: usize = 364; /// Number of CPU/GPU frame slots shared by the swapchain renderer. pub const VULKAN_FRAME_SLOT_COUNT: usize = 2; @@ -235,6 +236,10 @@ pub struct VulkanFrameUniforms { pub fog_end: f32, /// World-space camera position used for fog distance. pub camera_position: [f32; 3], + /// XY bounds of the original reflective faces: min X/Y, max X/Y. + pub embm_bounds: [f32; 4], + /// Native wave scroll fraction in the ten-second cycle. + pub embm_phase: f32, } impl VulkanFrameUniforms { @@ -258,6 +263,8 @@ impl VulkanFrameUniforms { fog_start: f32::MAX, fog_end: f32::MAX, camera_position: [0.0; 3], + embm_bounds: [0.0, 0.0, 1.0, 1.0], + embm_phase: 0.0, } } @@ -283,7 +290,14 @@ impl VulkanFrameUniforms { .chain(self.fog_color) .chain([self.fog_start, self.fog_end]) .chain(self.camera_position) + .chain(self.embm_bounds) + .chain([self.embm_phase]) .all(f32::is_finite) + && self.embm_bounds[2] > self.embm_bounds[0] + && self.embm_bounds[3] > self.embm_bounds[1] + && (self.embm_bounds[2] - self.embm_bounds[0]).is_finite() + && (self.embm_bounds[3] - self.embm_bounds[1]).is_finite() + && (0.0..1.0).contains(&self.embm_phase) && (self.fog_end <= self.fog_start || ((self.fog_start * self.fog_start).is_finite() && (self.fog_end * self.fog_end).is_finite())) @@ -364,6 +378,13 @@ impl VulkanFrameUniforms { self.camera_position[2], 0.0, ]); + values[92..96].copy_from_slice(&self.embm_bounds); + values[96..100].copy_from_slice(&[ + self.embm_phase, + fparkan_render::water::EMBM_BUMP_TILE, + fparkan_render::water::EMBM_COEFFICIENTS[0], + fparkan_render::water::EMBM_COEFFICIENTS[1], + ]); let mut bytes = [0_u8; VULKAN_FRAME_UNIFORM_BYTES]; for (index, value) in values.into_iter().enumerate() { let offset = index * std::mem::size_of::<f32>(); @@ -437,6 +458,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 +794,7 @@ impl VulkanStaticMesh { batch_flags: 0, pipeline_state: LegacyPipelineState::default(), terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, }], } @@ -859,6 +883,8 @@ mod static_mesh_tests { fog_start: 13.0, fog_end: 14.0, camera_position: [15.0, 16.0, 17.0], + embm_bounds: [100.0, 200.0, 300.0, 600.0], + embm_phase: 0.25, }; let bytes = frame.to_ne_bytes(); assert_eq!(bytes.len(), VULKAN_FRAME_UNIFORM_BYTES); @@ -877,6 +903,28 @@ mod static_mesh_tests { assert_eq!(read(320), 10.0); assert_eq!(read(336), 13.0); assert_eq!(read(352), 15.0); + assert_eq!(read(368), 100.0); + assert_eq!(read(380), 600.0); + assert_eq!(read(384), 0.25); + assert_eq!(read(388), 100.0); + assert_eq!(read(392), 0.01); + } + + #[test] + fn embm_uniforms_reject_invalid_bounds_and_phase() { + for (bounds, phase) in [ + ([0.0; 4], 0.0), + ([-f32::MAX, 0.0, f32::MAX, 1.0], 0.0), + ([0.0, 0.0, 1.0, 1.0], f32::NAN), + ([0.0, 0.0, 1.0, 1.0], 1.0), + ] { + assert!(!VulkanFrameUniforms { + embm_bounds: bounds, + embm_phase: phase, + ..Default::default() + } + .is_finite()); + } } #[test] @@ -1032,6 +1080,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 +1112,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 +1123,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 +1242,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 +1252,7 @@ mod static_mesh_tests { ..LegacyPipelineState::default() }, terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 128, ..base }; @@ -1211,6 +1264,7 @@ mod static_mesh_tests { ..LegacyPipelineState::default() }, terrain_base_only: false, + terrain_reflection: false, alpha_test_reference: 0, ..base }; @@ -1221,6 +1275,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 +1556,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..af20dfa 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,18 @@ 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, + reflection_enabled, + ) { + 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 +304,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 +341,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 +434,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 +501,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 +544,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 +566,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 +683,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 +708,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 +718,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 +746,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 +1013,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 +1297,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 +1363,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() { diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index b06bb26..4b2f7d2 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(); |
