diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 63 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 22 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 202 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 205 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 69 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 163 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 47 |
7 files changed, 689 insertions, 82 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 0de1381..3249012 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -865,6 +865,7 @@ fn append_node_fallback_batch( lightmap_index: batch.lightmap_index, batch_flags: batch.batch_flags, pipeline_state: world_pipeline_state(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); Ok(()) @@ -1205,20 +1206,23 @@ fn static_terrain_draw_ranges( let mut ranges = Vec::new(); let mut start_face = 0usize; while start_face < lod0.len() { - let material_index = terrain + let first_face = terrain .faces .get(lod0[start_face].face_index) - .ok_or(VulkanAssetMeshError::IndexOutOfRange)? - .material_tag; + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; + let material_index = first_face.material_tag; + let terrain_base_only = first_face.flags.0 & 0x400 == 0; let mut end_face = start_face + 1; - while end_face < lod0.len() - && terrain + while end_face < lod0.len() { + let next_face = terrain .faces .get(lod0[end_face].face_index) - .ok_or(VulkanAssetMeshError::IndexOutOfRange)? - .material_tag - == material_index - { + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; + if next_face.material_tag != material_index + || (next_face.flags.0 & 0x400 == 0) != terrain_base_only + { + break; + } end_face += 1; } ranges.push(VulkanStaticDrawRange { @@ -1234,6 +1238,7 @@ fn static_terrain_draw_ranges( lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: world_pipeline_state(), + terrain_base_only, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); start_face = end_face; @@ -1279,6 +1284,7 @@ fn static_model_indices_and_ranges( lightmap_index: batch.lightmap_index, batch_flags: batch.batch_flags, pipeline_state: world_pipeline_state(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }); } @@ -1481,6 +1487,7 @@ mod tests { lightmap_index: 0, batch_flags: 0, pipeline_state: world_pipeline_state(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }] ); @@ -2002,6 +2009,7 @@ mod tests { lightmap_index: 0, batch_flags: 0, pipeline_state: world_pipeline_state(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }, @@ -2012,6 +2020,7 @@ mod tests { lightmap_index: 0, batch_flags: 0, pipeline_state: world_pipeline_state(), + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, }, @@ -2168,8 +2177,16 @@ mod tests { } fn terrain_face_with_tag(material_tag: u16, vertices: [u16; 3]) -> TerrainFace28 { + terrain_face_with_tag_and_flags(material_tag, 0, vertices) + } + + fn terrain_face_with_tag_and_flags( + material_tag: u16, + flags: u32, + vertices: [u16; 3], + ) -> TerrainFace28 { TerrainFace28 { - flags: FullSurfaceMask(0), + flags: FullSurfaceMask(flags), material_tag, aux_tag: 0, vertices, @@ -2201,7 +2218,7 @@ mod tests { } #[test] - fn terrain_ranges_preserve_face_order_and_packed_material_tags() { + fn terrain_ranges_merge_only_contiguous_equal_material_and_surface_mode() { let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), @@ -2216,27 +2233,35 @@ mod tests { aux14: Vec::new(), aux18: Vec::new(), faces: vec![ - terrain_face_with_tag(0xff01, [0, 1, 2]), - terrain_face_with_tag(0xff01, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0, [0, 1, 2]), terrain_face_with_tag(0x0102, [0, 1, 2]), - terrain_face_with_tag(0xff01, [0, 1, 2]), + terrain_face_with_tag_and_flags(0xff00, 0x400, [0, 1, 2]), ], }; - set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]); + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 5]]); let lod0 = static_terrain_lod0_assignments(&terrain).expect("terrain LOD0"); let ranges = static_terrain_draw_ranges(&terrain, &lod0).expect("representable terrain"); - assert_eq!(ranges.len(), 3); + assert_eq!(ranges.len(), 4); assert_eq!(ranges[0].first_index, 0); assert_eq!(ranges[0].index_count, 6); - assert_eq!(ranges[0].material_index, 0xff01); + assert_eq!(ranges[0].material_index, 0xff00); + assert!(!ranges[0].terrain_base_only); assert_eq!(ranges[1].first_index, 6); assert_eq!(ranges[1].index_count, 3); - assert_eq!(ranges[1].material_index, 0x0102); + assert_eq!(ranges[1].material_index, 0xff00); + assert!(ranges[1].terrain_base_only); assert_eq!(ranges[2].first_index, 9); assert_eq!(ranges[2].index_count, 3); - assert_eq!(ranges[2].material_index, 0xff01); + assert_eq!(ranges[2].material_index, 0x0102); + assert!(ranges[2].terrain_base_only); + assert_eq!(ranges[3].first_index, 12); + assert_eq!(ranges[3].index_count, 3); + assert_eq!(ranges[3].material_index, 0xff00); + assert!(!ranges[3].terrain_base_only); assert!(ranges.iter().all(|range| { range.alpha_test_reference == VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE })); diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 7cf192c..9584405 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -51,12 +51,13 @@ use self::instance::{cstring_vec, ensure_instance_extensions_available}; use self::resources::{ color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync, create_frame_uniform_buffer, create_index_buffer, create_material_specular_buffer, - create_readback_buffer, create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, - create_static_texture_image, create_vertex_buffer, destroy_allocated_buffer, - destroy_allocated_image, destroy_depth_attachment, readback_buffer_bytes, - update_static_texture_image, write_frame_uniform, write_material_specular_buffer, - write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, - VulkanAllocatedImage, VulkanDepthAttachment, VulkanFrameSync, + create_point_light_buffer, create_readback_buffer, create_static_mesh_index_buffer, + create_static_mesh_vertex_buffer, create_static_texture_image, create_vertex_buffer, + destroy_allocated_buffer, destroy_allocated_image, destroy_depth_attachment, + readback_buffer_bytes, update_static_texture_image, write_frame_uniform, + write_material_specular_buffer, write_point_light_buffer, write_static_mesh_index_buffer, + write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, VulkanAllocatedImage, + VulkanDepthAttachment, VulkanFrameSync, }; pub use self::runtime::{ create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request, @@ -69,8 +70,9 @@ pub use self::smoke_types::{ VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanStaticTextureMip, VulkanStaticVertex, VulkanValidationReport, - VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_UNIFORM_BYTES, - VULKAN_MATERIAL_SPECULAR_RECORD_BYTES, + VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_SLOT_COUNT, VULKAN_FRAME_UNIFORM_BYTES, + VULKAN_MATERIAL_SPECULAR_RECORD_BYTES, VULKAN_POINT_LIGHT_HEADER_BYTES, + VULKAN_POINT_LIGHT_RECORD_BYTES, }; #[cfg(test)] use self::surface::extension_name; @@ -83,8 +85,8 @@ pub use self::swapchain::{ VulkanSwapchainProbeError, VulkanSwapchainReport, }; use self::swapchain_resources::{ - create_swapchain_resources, destroy_swapchain_resources, update_material_texture_descriptor, - VulkanSwapchainResources, + create_swapchain_resources, destroy_swapchain_resources, ensure_point_light_capacity, + update_material_texture_descriptor, VulkanSwapchainResources, }; use self::validation::{create_validation_messenger, VulkanValidationMessenger}; use ash::vk; diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index b841588..0b43565 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -4,8 +4,8 @@ use ash::vk; use fparkan_platform::DepthStencilSupport; use super::{ - VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh, - VulkanStaticTexture, + VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanPointLight, VulkanSmokeRendererError, + VulkanStaticMesh, VulkanStaticTexture, }; use crate::select_depth_stencil_attachment_format; @@ -520,7 +520,6 @@ pub(super) fn create_frame_uniform_buffer( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, ) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> { - const FRAME_SLOTS: u64 = 2; const FRAME_BYTES: u64 = super::VULKAN_FRAME_UNIFORM_BYTES as u64; let limits = // SAFETY: The physical device belongs to this live instance and the query returns a value copy. @@ -532,12 +531,11 @@ pub(super) fn create_frame_uniform_buffer( }, )? / alignment * alignment; - let size = - stride - .checked_mul(FRAME_SLOTS) - .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { - context: "frame uniform buffer size overflow", - })?; + let size = stride + .checked_mul(super::VULKAN_FRAME_SLOT_COUNT as u64) + .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { + context: "frame uniform buffer size overflow", + })?; let bytes = vec![ 0_u8; usize::try_from(size).map_err(|_| { @@ -556,6 +554,192 @@ pub(super) fn create_frame_uniform_buffer( Ok((buffer, stride)) } +/// Returns the per-frame descriptor range, aligned slot stride, total bytes, +/// and maximum light count accepted by the device's storage-buffer limit. +pub(super) fn point_light_buffer_layout( + instance: &VulkanInstanceProbe, + device: &VulkanLogicalDeviceProbe, + capacity: usize, +) -> Result<(u64, u64, u64, usize), VulkanSmokeRendererError> { + let limits = + // SAFETY: The physical device belongs to this live instance and the query returns a value copy. + unsafe { instance.instance.get_physical_device_properties(device.physical_device()) } + .limits; + let max_range = u64::from(limits.max_storage_buffer_range); + let header_bytes = super::VULKAN_POINT_LIGHT_HEADER_BYTES as u64; + let record_bytes = super::VULKAN_POINT_LIGHT_RECORD_BYTES as u64; + if max_range < header_bytes { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "device storage-buffer range cannot hold the point-light header", + }); + } + let max_count = usize::try_from((max_range - header_bytes) / record_bytes).map_err(|_| { + VulkanSmokeRendererError::InvalidPointLights { + context: "device point-light capacity exceeds address space", + } + })?; + if capacity > max_count { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light count exceeds maxStorageBufferRange", + }); + } + let capacity_u64 = + u64::try_from(capacity).map_err(|_| VulkanSmokeRendererError::InvalidPointLights { + context: "point-light capacity exceeds address space", + })?; + let range = header_bytes + .checked_add(record_bytes.checked_mul(capacity_u64).ok_or( + VulkanSmokeRendererError::InvalidPointLights { + context: "point-light descriptor range overflow", + }, + )?) + .ok_or(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light descriptor range overflow", + })?; + let alignment = u64::from(limits.min_storage_buffer_offset_alignment).max(1); + let stride = + range + .checked_add(alignment - 1) + .ok_or(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light frame stride overflow", + })? + / alignment + * alignment; + let total = stride + .checked_mul(u64::try_from(super::VULKAN_FRAME_SLOT_COUNT).unwrap_or(u64::MAX)) + .ok_or(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light buffer size overflow", + })?; + if stride + .checked_mul(u64::try_from(super::VULKAN_FRAME_SLOT_COUNT - 1).unwrap_or(u64::MAX)) + .is_none_or(|last_offset| last_offset > u64::from(u32::MAX)) + { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light dynamic offset exceeds Vulkan's 32-bit limit", + }); + } + Ok((range, stride, total, max_count)) +} + +/// Allocates a fence-safe point-light SSBO with one aligned segment per frame. +pub(super) fn create_point_light_buffer( + instance: &VulkanInstanceProbe, + device: &VulkanLogicalDeviceProbe, + capacity: usize, +) -> Result<(VulkanAllocatedBuffer, u64, u64, usize), VulkanSmokeRendererError> { + let (range, stride, total, max_count) = point_light_buffer_layout(instance, device, capacity)?; + let bytes = + vec![ + 0_u8; + usize::try_from(total).map_err(|_| VulkanSmokeRendererError::InvalidPointLights { + context: "point-light buffer size exceeds address space", + })? + ]; + let buffer = create_host_visible_buffer( + instance, + device, + &bytes, + vk::BufferUsageFlags::STORAGE_BUFFER, + "point-light storage buffer", + )?; + Ok((buffer, range, stride, max_count)) +} + +pub(super) fn encode_point_lights( + lights: &[VulkanPointLight], +) -> Result<Vec<u8>, VulkanSmokeRendererError> { + let count = + u32::try_from(lights.len()).map_err(|_| VulkanSmokeRendererError::InvalidPointLights { + context: "point-light count exceeds the storage header", + })?; + let total = super::VULKAN_POINT_LIGHT_HEADER_BYTES + .checked_add( + super::VULKAN_POINT_LIGHT_RECORD_BYTES + .checked_mul(lights.len()) + .ok_or(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light upload size overflow", + })?, + ) + .ok_or(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light upload size overflow", + })?; + let mut bytes = vec![0; total]; + bytes[0..4].copy_from_slice(&count.to_ne_bytes()); + let mut offset = super::VULKAN_POINT_LIGHT_HEADER_BYTES; + for light in lights { + for value in light + .position + .into_iter() + .chain([light.range]) + .chain(light.rgb) + .chain([if light.active { 1.0 } else { 0.0 }]) + .chain(light.coefficients) + .chain([if light.active { 1.0 } else { 0.0 }]) + { + bytes[offset..offset + std::mem::size_of::<f32>()] + .copy_from_slice(&value.to_ne_bytes()); + offset += std::mem::size_of::<f32>(); + } + } + debug_assert_eq!(offset, total); + Ok(bytes) +} + +/// Uploads the current light set into the frame slot whose fence has completed. +pub(super) fn write_point_light_buffer( + device: &VulkanLogicalDeviceProbe, + buffer: &VulkanAllocatedBuffer, + stride: u64, + frame_index: usize, + lights: &[VulkanPointLight], +) -> Result<(), VulkanSmokeRendererError> { + let slot = + u64::try_from(frame_index).map_err(|_| VulkanSmokeRendererError::InvariantViolation { + context: "point-light frame slot index", + })?; + if frame_index >= super::VULKAN_FRAME_SLOT_COUNT { + return Err(VulkanSmokeRendererError::InvariantViolation { + context: "point-light frame slot is out of bounds", + }); + } + let offset = stride + .checked_mul(slot) + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "point-light frame slot offset", + })?; + let bytes = encode_point_lights(lights)?; + if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > stride { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light upload exceeds the allocated frame slot", + }); + } + let total = stride + .checked_mul(u64::try_from(super::VULKAN_FRAME_SLOT_COUNT).unwrap_or(u64::MAX)) + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "point-light buffer mapped size", + })?; + let offset = + usize::try_from(offset).map_err(|_| VulkanSmokeRendererError::InvariantViolation { + context: "point-light frame slot offset exceeds address space", + })?; + // Map the whole allocation because the Vulkan minimum map alignment may + // exceed minStorageBufferOffsetAlignment used by the dynamic frame offset. + let mapped = unsafe { + device + .device() + .map_memory(buffer.memory, 0, total, vk::MemoryMapFlags::empty()) + } + .map_err(|result| VulkanSmokeRendererError::VulkanOperation { + context: "vkMapMemory(point-light buffer)", + result, + })?; + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>().add(offset), bytes.len()); + device.device().unmap_memory(buffer.memory); + } + Ok(()) +} + /// Allocates one aligned storage-buffer record for every material descriptor. /// /// The descriptor uses a dynamic storage-buffer offset. Keeping the stride diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index c9de5ba..30a42dd 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -3,7 +3,7 @@ use ash::vk; use fparkan_render::{LegacyDepthMode, LegacyPipelineState}; -use super::smoke_types::resolve_draw_texture_indices; +use super::smoke_types::{resolve_draw_texture_indices, VulkanStaticMaterial}; use super::update_material_texture_descriptor; use super::{ create_command_pool, create_frame_sync, create_index_buffer, create_static_mesh_index_buffer, @@ -11,14 +11,14 @@ use super::{ create_validation_messenger, create_vertex_buffer, create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image, - destroy_swapchain_resources, plan_vulkan_surface, readback_buffer_bytes, - update_static_texture_image, write_frame_uniform, write_material_specular_buffer, - write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, - VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanReadbackArtifact, - VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, - VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, - VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger, - VulkanValidationReport, + destroy_swapchain_resources, ensure_point_light_capacity, plan_vulkan_surface, + readback_buffer_bytes, update_static_texture_image, write_frame_uniform, + write_material_specular_buffer, write_point_light_buffer, write_static_mesh_index_buffer, + write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, VulkanInstanceConfig, + VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanReadbackArtifact, VulkanSmokeFrameOutcome, + VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, + VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe, + VulkanSwapchainResources, VulkanValidationMessenger, VulkanValidationReport, }; use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION; use crate::shader::validate_triangle_shaders; @@ -361,18 +361,7 @@ impl VulkanSmokeRenderer { create_info .materials .iter() - .map(|material| { - let combiner_mode = if material.lightmap_mode { - 3.0 - } else if material.sky_nebula_stars { - 2.0 - } else if material.unlit { - 1.0 - } else { - 0.0 - }; - combiner_mode + if material.sky_far_depth { 4.0 } else { 0.0 } - }) + .map(VulkanStaticMaterial::encoded_material_mode) .collect() }; let material_specular = if create_info.materials.is_empty() { @@ -451,6 +440,8 @@ impl VulkanSmokeRenderer { readback_enabled: false, camera: create_info.camera, frame_uniforms: super::VulkanFrameUniforms::from_camera(create_info.camera), + point_lights: Vec::new(), + point_lights_explicit: false, frame_sync: Vec::new(), images_in_flight: Vec::new(), current_frame: 0, @@ -587,6 +578,41 @@ impl VulkanSmokeRenderer { Ok(()) } + /// Replaces the complete dynamic point-light set for subsequent frames. + /// + /// The storage buffer grows on demand and rejects counts beyond the + /// device's `maxStorageBufferRange`; it never truncates scene lights. + /// Values are uploaded to the current fence-safe frame slot by + /// [`Self::draw_frame`]. + pub fn set_point_lights( + &mut self, + point_lights: &[super::VulkanPointLight], + ) -> Result<(), VulkanSmokeRendererError> { + let max_count = self.resources_ref()?.max_point_light_count; + if point_lights.len() > max_count { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light count exceeds maxStorageBufferRange", + }); + } + if point_lights.iter().any(|light| { + !light + .position + .into_iter() + .chain(light.rgb) + .chain([light.range]) + .chain(light.coefficients) + .all(f32::is_finite) + }) { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "non-finite point-light field", + }); + } + self.point_lights.clear(); + self.point_lights.extend_from_slice(point_lights); + self.point_lights_explicit = true; + Ok(()) + } + /// Enables the optional swapchain image copy used by screenshot/readback /// callers. Runtime frames leave it disabled to avoid a transfer every /// frame. @@ -1226,6 +1252,34 @@ impl VulkanSmokeRenderer { self.shutdown_inner() } + /// Returns the latest submitted frame using the existing optional readback copy. + /// + /// This synchronizes the device before mapping the host-coherent buffer, so + /// callers should use it only for bounded diagnostic captures. + pub fn readback_last_submitted_frame( + &self, + ) -> Result<Option<VulkanReadbackArtifact>, VulkanSmokeRendererError> { + let device = self.device_ref()?; + // SAFETY: The renderer owns this device and all submitted work; idling + // prevents the last readback transfer from racing the host buffer map. + unsafe { device.device().device_wait_idle() }.map_err(|error| { + VulkanSmokeRendererError::VulkanOperation { + context: "vkDeviceWaitIdle(readback frame)", + result: error, + } + })?; + let Some(bytes) = self.completed_readback(device)? else { + return Ok(None); + }; + let format = self.swapchain_ref()?.report.plan.format.format; + Ok(Some(VulkanReadbackArtifact { + format, + width: self.report.swapchain_extent.0, + height: self.report.swapchain_extent.1, + bytes, + })) + } + /// Requests swapchain recreation for a new drawable extent. pub fn request_resize(&mut self, extent: (u32, u32)) { self.pending_extent = Some(extent); @@ -1329,6 +1383,44 @@ impl VulkanSmokeRenderer { context: "vkWaitForFences", result: error, })?; + let legacy_point_light = self.frame_uniforms.point_light; + let point_light_count = if self.point_lights_explicit { + self.point_lights.len() + } else { + usize::from(legacy_point_light.active) + }; + let needs_point_light_growth = + point_light_count > self.resources_ref()?.point_light_capacity; + if needs_point_light_growth { + // Updating every material's binding 6 and replacing the shared + // SSBO requires all prior command buffers to finish. + // SAFETY: The renderer owns the logical device and no submission + // can observe the replacement after this idle point. + unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| { + VulkanSmokeRendererError::VulkanOperation { + context: "vkDeviceWaitIdle(point-light buffer growth)", + result, + } + })?; + let instance = + self.instance + .as_ref() + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "Vulkan instance during point-light growth", + })?; + let device = + self.device + .as_ref() + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "logical device during point-light growth", + })?; + let resources = self.swapchain_resources.as_mut().ok_or( + VulkanSmokeRendererError::InvariantViolation { + context: "swapchain resources during point-light growth", + }, + )?; + ensure_point_light_capacity(instance, device, resources, point_light_count)?; + } if self.material_specular_dirty { // The storage buffer is shared by all in-flight frames. Wait for // every prior use before replacing the records so a setter cannot @@ -1388,14 +1480,42 @@ impl VulkanSmokeRenderer { } self.images_in_flight[image_index_usize] = in_flight_fence; let frame_bytes = self.frame_uniforms.to_ne_bytes(); - let resources = self.resources_ref()?; - write_frame_uniform( - self.device_ref()?, - &resources.frame_uniform_buffer, - resources.frame_uniform_stride, - self.current_frame, - &frame_bytes, - )?; + { + let explicit_point_lights = self.point_lights_explicit; + let legacy_point_light = self.frame_uniforms.point_light; + let point_lights = if explicit_point_lights { + self.point_lights.as_slice() + } else if legacy_point_light.active { + std::slice::from_ref(&legacy_point_light) + } else { + &[] + }; + let device = + self.device + .as_ref() + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "logical device during frame upload", + })?; + let resources = self.swapchain_resources.as_ref().ok_or( + VulkanSmokeRendererError::InvariantViolation { + context: "swapchain resources during frame upload", + }, + )?; + write_frame_uniform( + device, + &resources.frame_uniform_buffer, + resources.frame_uniform_stride, + self.current_frame, + &frame_bytes, + )?; + write_point_light_buffer( + device, + &resources.point_light_buffer, + resources.point_light_stride, + self.current_frame, + point_lights, + )?; + } let render_finished = self .resources_ref()? .render_finished @@ -1704,6 +1824,12 @@ impl VulkanSmokeRenderer { .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", @@ -1715,7 +1841,7 @@ impl VulkanSmokeRenderer { resources.pipeline_layout, 0, std::slice::from_ref(descriptor_set), - &[frame_offset, material_offset], + &[frame_offset, material_offset, point_light_offset], ); let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes(); device.device().cmd_push_constants( @@ -1776,18 +1902,23 @@ impl VulkanSmokeRenderer { 60, &material_uv_transforms, ); - let unlit = self.material_unlit.get(texture_index).ok_or( + let material_mode = self.material_unlit.get(texture_index).ok_or( VulkanSmokeRendererError::InvariantViolation { context: "static material unlit flag", }, )?; - let unlit = [unlit.to_ne_bytes()]; + let material_mode = if range.terrain_base_only { + ((*material_mode).round() as i32 | 16) as f32 + } else { + *material_mode + }; + 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, - &unlit.concat(), + &material_mode.concat(), ); device.device().cmd_draw_indexed( command_buffer, @@ -2451,6 +2582,7 @@ mod tests { lightmap_index: 4, batch_flags: 0x108, pipeline_state: base_state, + terrain_base_only: false, alpha_test_reference: 17, }, VulkanStaticDrawRange { @@ -2460,6 +2592,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, ]; @@ -2643,6 +2776,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }) .collect::<Vec<_>>(); @@ -2666,6 +2800,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { @@ -2675,6 +2810,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, super::super::VulkanStaticDrawRange { @@ -2684,6 +2820,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, ]; @@ -2707,6 +2844,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }) .collect::<Vec<_>>(); @@ -2735,6 +2873,7 @@ mod tests { lightmap_index: u8::MAX, batch_flags: 0, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: 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 f628e23..51f8384 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -194,6 +194,15 @@ impl Default for VulkanPointLight { /// state. pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 368; +/// Number of CPU/GPU frame slots shared by the swapchain renderer. +pub const VULKAN_FRAME_SLOT_COUNT: usize = 2; + +/// `uvec4` header bytes for the dynamic point-light storage buffer. +pub const VULKAN_POINT_LIGHT_HEADER_BYTES: usize = 16; + +/// Three `vec4` records per dynamically uploaded point light. +pub const VULKAN_POINT_LIGHT_RECORD_BYTES: usize = 3 * 4 * std::mem::size_of::<f32>(); + /// Bytes occupied by one material's base and overlay specular records. /// /// Each record is one `vec4`: RGB followed by the native integer power stored @@ -413,6 +422,8 @@ pub struct VulkanStaticDrawRange { /// Bits `0x100` and `0x008` opt a range into that queue independently of /// the material blend category. pub batch_flags: u16, + /// Terrain ranges that use the native lit base-only path (face flag 0x400 clear). + pub terrain_base_only: 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. @@ -505,6 +516,33 @@ pub struct VulkanStaticMaterial { /// renderer projection choice; it is not a claim about the original D3D /// depth-bit encoding. pub sky_far_depth: bool, + /// Push material mode bit 8 and treat positions as Vulkan clip-space + /// `XYZRHW` vertices. Their x/y coordinates are converted from viewport + /// pixels by the caller and z retains the native `0.999` depth. + pub screen_tl: bool, +} + +impl VulkanStaticMaterial { + pub(crate) fn encoded_material_mode(&self) -> f32 { + let combiner_mode = if self.screen_tl { + 1.0 + } else if self.lightmap_mode { + 3.0 + } else if self.sky_nebula_stars { + 2.0 + } else if self.unlit { + 1.0 + } else { + 0.0 + }; + combiner_mode + + if self.sky_far_depth && !self.screen_tl { + 4.0 + } else { + 0.0 + } + + if self.screen_tl { 8.0 } else { 0.0 } + } } /// One authored RGBA8 mip level accepted by the Vulkan texture upload path. @@ -718,6 +756,7 @@ impl VulkanStaticMesh { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }], @@ -953,6 +992,7 @@ mod static_mesh_tests { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }], @@ -983,6 +1023,7 @@ mod static_mesh_tests { material_index: 7, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }, @@ -992,6 +1033,7 @@ mod static_mesh_tests { material_index: 2, lightmap_index: u8::MAX, batch_flags: 0, + terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), alpha_test_reference: 0, }, @@ -1028,6 +1070,7 @@ mod static_mesh_tests { sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }, VulkanStaticMaterial { material_index: 7, @@ -1054,9 +1097,19 @@ mod static_mesh_tests { sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }, ]; + let mut screen_tl = materials[0].clone(); + screen_tl.screen_tl = true; + screen_tl.sky_far_depth = true; + assert_eq!(screen_tl.encoded_material_mode(), 9.0); + let mut far_sky = materials[0].clone(); + far_sky.unlit = true; + far_sky.sky_far_depth = true; + assert_eq!(far_sky.encoded_material_mode(), 5.0); + assert_eq!( resolve_draw_texture_indices(&ranges, &materials), Ok(vec![1, 0]) @@ -1248,7 +1301,7 @@ pub struct VulkanValidationReport { pub vuids: Vec<String>, } -/// CPU-owned copy of the final completed swapchain image readback. +/// CPU-owned copy of a completed swapchain image readback. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanReadbackArtifact { /// Vulkan swapchain format as a raw enum value. @@ -1257,8 +1310,8 @@ pub struct VulkanReadbackArtifact { pub width: u32, /// Height of each image in pixels. pub height: u32, - /// One raw four-byte-per-pixel image: the last successfully submitted - /// swapchain image before synchronized teardown. + /// One raw four-byte-per-pixel image from the last successfully submitted + /// swapchain image at the synchronization point. pub bytes: Vec<u8>, } @@ -1321,6 +1374,11 @@ pub enum VulkanSmokeRendererError { /// Validation failure detail. context: &'static str, }, + /// Submitted dynamic point-light data exceeds the Vulkan storage range or contains invalid values. + InvalidPointLights { + /// Validation failure detail. + context: &'static str, + }, /// The submitted static texture cannot be represented by this path. InvalidStaticTexture { /// Validation failure detail. @@ -1351,6 +1409,7 @@ impl std::fmt::Display for VulkanSmokeRendererError { Self::InvalidStaticCamera { context } => { write!(f, "invalid static camera: {context}") } + Self::InvalidPointLights { context } => write!(f, "invalid point lights: {context}"), Self::InvalidStaticTexture { context } => { write!(f, "invalid static texture: {context}") } @@ -1416,6 +1475,10 @@ pub struct VulkanSmokeRenderer { pub(super) readback_enabled: bool, pub(super) camera: VulkanStaticCamera, pub(super) frame_uniforms: VulkanFrameUniforms, + /// Dynamically sized point-light records submitted by the client. + pub(super) point_lights: Vec<VulkanPointLight>, + /// Whether `set_point_lights` has replaced legacy `frame_uniforms.point_light`. + pub(super) point_lights_explicit: bool, pub(super) frame_sync: Vec<VulkanFrameSync>, pub(super) images_in_flight: Vec<vk::Fence>, pub(super) current_frame: usize, diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index b8049d3..89ca43a 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -9,9 +9,10 @@ use std::collections::BTreeMap; use super::{ color_subresource_range, create_depth_attachment, create_material_specular_buffer, - destroy_depth_attachment, VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment, - VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe, - TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES, + create_point_light_buffer, destroy_depth_attachment, VulkanAllocatedBuffer, + VulkanAllocatedImage, VulkanDepthAttachment, VulkanInstanceProbe, VulkanLogicalDeviceProbe, + VulkanSmokeRendererError, VulkanSwapchainProbe, TRIANGLE_FRAGMENT_SHADER_WORDS, + TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES, VULKAN_MATERIAL_SPECULAR_RECORD_BYTES, }; @@ -33,6 +34,11 @@ pub(super) struct VulkanSwapchainResources { pub(super) frame_uniform_stride: u64, pub(super) material_specular_buffer: VulkanAllocatedBuffer, pub(super) material_specular_stride: u64, + pub(super) point_light_buffer: VulkanAllocatedBuffer, + pub(super) point_light_range: u64, + pub(super) point_light_stride: u64, + pub(super) point_light_capacity: usize, + pub(super) max_point_light_count: usize, /// Live graphics pipelines indexed by canonical backend-neutral state. pub(super) pipelines: BTreeMap<PipelineKey, vk::Pipeline>, pub(super) framebuffers: Vec<vk::Framebuffer>, @@ -73,6 +79,71 @@ pub(super) fn update_material_texture_descriptor( Ok(()) } +/// Grows the dynamic light SSBO when the scene adds lights beyond its current +/// capacity. The caller must idle the device before updating these descriptors. +pub(super) fn ensure_point_light_capacity( + instance: &VulkanInstanceProbe, + device: &VulkanLogicalDeviceProbe, + resources: &mut VulkanSwapchainResources, + needed: usize, +) -> Result<(), VulkanSmokeRendererError> { + if needed <= resources.point_light_capacity { + return Ok(()); + } + if needed > resources.max_point_light_count { + return Err(VulkanSmokeRendererError::InvalidPointLights { + context: "point-light count exceeds maxStorageBufferRange", + }); + } + let target = resources + .point_light_capacity + .saturating_mul(2) + .max(1) + .max(needed) + .min(resources.max_point_light_count); + let (new_buffer, new_range, new_stride, max_count) = + create_point_light_buffer(instance, device, target)?; + if max_count != resources.max_point_light_count { + unsafe { + device.device().destroy_buffer(new_buffer.buffer, None); + device.device().free_memory(new_buffer.memory, None); + } + return Err(VulkanSmokeRendererError::InvariantViolation { + context: "point-light capacity changed for the active device", + }); + } + let info = vk::DescriptorBufferInfo::default() + .buffer(new_buffer.buffer) + .offset(0) + .range(new_range); + let writes = resources + .descriptor_sets + .iter() + .copied() + .map(|descriptor_set| { + vk::WriteDescriptorSet::default() + .dst_set(descriptor_set) + .dst_binding(6) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC) + .buffer_info(std::slice::from_ref(&info)) + }) + .collect::<Vec<_>>(); + // SAFETY: The caller waited for device idle before replacing a descriptor + // that submitted command buffers may reference. + unsafe { device.device().update_descriptor_sets(&writes, &[]) }; + let old_buffer = std::mem::replace(&mut resources.point_light_buffer, new_buffer); + resources.point_light_range = new_range; + resources.point_light_stride = new_stride; + resources.point_light_capacity = target; + // SAFETY: All prior submissions have completed and descriptors now point + // at the replacement allocation. + unsafe { + device.device().destroy_buffer(old_buffer.buffer, None); + device.device().free_memory(old_buffer.memory, None); + } + Ok(()) +} + struct PartialSwapchainResources { image_views: Vec<vk::ImageView>, readback_buffers: Vec<VulkanAllocatedBuffer>, @@ -87,6 +158,11 @@ struct PartialSwapchainResources { frame_uniform_stride: u64, material_specular_buffer: Option<VulkanAllocatedBuffer>, material_specular_stride: u64, + point_light_buffer: Option<VulkanAllocatedBuffer>, + point_light_range: u64, + point_light_stride: u64, + point_light_capacity: usize, + max_point_light_count: usize, pipelines: BTreeMap<PipelineKey, vk::Pipeline>, framebuffers: Vec<vk::Framebuffer>, command_buffers: Vec<vk::CommandBuffer>, @@ -133,6 +209,11 @@ pub(super) fn create_swapchain_resources( frame_uniform_stride: 0, material_specular_buffer: None, material_specular_stride: 0, + point_light_buffer: None, + point_light_range: 0, + point_light_stride: 0, + point_light_capacity: 0, + max_point_light_count: 0, pipelines: BTreeMap::new(), framebuffers: Vec::new(), command_buffers: Vec::new(), @@ -173,6 +254,20 @@ pub(super) fn create_swapchain_resources( }; partial.material_specular_stride = material_specular_stride; partial.material_specular_buffer = Some(material_specular_buffer); + let point_light_capacity = 1; + let (point_light_buffer, point_light_range, point_light_stride, max_point_light_count) = + match create_point_light_buffer(instance, device, point_light_capacity) { + Ok(buffer) => buffer, + Err(error) => { + destroy_partial_swapchain_resources(device, command_pool, partial); + return Err(error); + } + }; + partial.point_light_range = point_light_range; + partial.point_light_stride = point_light_stride; + partial.point_light_capacity = point_light_capacity; + partial.max_point_light_count = max_point_light_count; + partial.point_light_buffer = Some(point_light_buffer); let ( depth_attachment, render_pass, @@ -199,6 +294,11 @@ pub(super) fn create_swapchain_resources( .material_specular_buffer .as_ref() .expect("material specular buffer is present before descriptor creation"), + partial + .point_light_buffer + .as_ref() + .expect("point-light buffer is present before descriptor creation"), + partial.point_light_range, ) { Ok(bundle) => bundle, Err(error) => { @@ -320,6 +420,13 @@ pub(super) fn create_swapchain_resources( .material_specular_buffer .expect("material specular buffer is present after successful swapchain setup"), material_specular_stride: partial.material_specular_stride, + point_light_buffer: partial + .point_light_buffer + .expect("point-light buffer is present after successful swapchain setup"), + point_light_range: partial.point_light_range, + point_light_stride: partial.point_light_stride, + point_light_capacity: partial.point_light_capacity, + max_point_light_count: partial.max_point_light_count, pipelines: partial.pipelines, framebuffers: partial.framebuffers, command_buffers: partial.command_buffers, @@ -349,6 +456,8 @@ fn create_swapchain_pipeline_bundle( depth_request: DepthStencilSupport, frame_uniform_buffer: &VulkanAllocatedBuffer, material_specular_buffer: &VulkanAllocatedBuffer, + point_light_buffer: &VulkanAllocatedBuffer, + point_light_range: u64, ) -> Result< ( VulkanDepthAttachment, @@ -377,6 +486,8 @@ fn create_swapchain_pipeline_bundle( texture_slot_indices, frame_uniform_buffer, material_specular_buffer, + point_light_buffer, + point_light_range, ) .inspect_err(|_| { // SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure. @@ -603,6 +714,8 @@ fn create_texture_descriptor_bundle( texture_slot_indices: &[usize], frame_uniform_buffer: &VulkanAllocatedBuffer, material_specular_buffer: &VulkanAllocatedBuffer, + point_light_buffer: &VulkanAllocatedBuffer, + point_light_range: u64, ) -> Result< ( vk::DescriptorSetLayout, @@ -615,6 +728,7 @@ fn create_texture_descriptor_bundle( const TEXTURES_PER_MATERIAL: usize = 4; const FRAME_BINDING: u32 = 4; const MATERIAL_SPECULAR_BINDING: u32 = 5; + const POINT_LIGHT_BINDING: u32 = 6; let mut bindings = (0..TEXTURES_PER_MATERIAL) .map(|binding| { vk::DescriptorSetLayoutBinding::default() @@ -638,6 +752,13 @@ fn create_texture_descriptor_bundle( .descriptor_count(1) .stage_flags(vk::ShaderStageFlags::VERTEX), ); + bindings.push( + vk::DescriptorSetLayoutBinding::default() + .binding(POINT_LIGHT_BINDING) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX), + ); let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); // SAFETY: The layout description is stack-owned and references no external memory. let layout = unsafe { @@ -676,10 +797,17 @@ fn create_texture_descriptor_bundle( let frame_pool_size = vk::DescriptorPoolSize::default() .ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC) .descriptor_count(material_count); - let material_specular_pool_size = vk::DescriptorPoolSize::default() + let storage_pool_size = vk::DescriptorPoolSize::default() .ty(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC) - .descriptor_count(material_count); - let pool_sizes = [pool_size, frame_pool_size, material_specular_pool_size]; + .descriptor_count(material_count.checked_mul(2).ok_or_else(|| { + // SAFETY: The layout was created above on this device and is + // rolled back before reporting an impossible descriptor count. + unsafe { device.device().destroy_descriptor_set_layout(layout, None) }; + VulkanSmokeRendererError::InvariantViolation { + context: "dynamic storage descriptor count overflow", + } + })?); + let pool_sizes = [pool_size, frame_pool_size, storage_pool_size]; let pool_info = vk::DescriptorPoolCreateInfo::default() .pool_sizes(&pool_sizes) .max_sets(material_count); @@ -751,7 +879,11 @@ fn create_texture_descriptor_bundle( .buffer(material_specular_buffer.buffer) .offset(0) .range(VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64); - let mut writes = Vec::with_capacity(descriptor_sets.len() * (TEXTURES_PER_MATERIAL + 2)); + let point_light_info = vk::DescriptorBufferInfo::default() + .buffer(point_light_buffer.buffer) + .offset(0) + .range(point_light_range); + let mut writes = Vec::with_capacity(descriptor_sets.len() * (TEXTURES_PER_MATERIAL + 3)); for (set_index, descriptor_set) in descriptor_sets.iter().copied().enumerate() { for binding in 0..TEXTURES_PER_MATERIAL { let image_info = &image_infos[set_index * TEXTURES_PER_MATERIAL + binding]; @@ -777,6 +909,13 @@ fn create_texture_descriptor_bundle( .descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC) .buffer_info(std::slice::from_ref(&material_specular_info)), ); + writes.push( + vk::WriteDescriptorSet::default() + .dst_set(descriptor_set) + .dst_binding(POINT_LIGHT_BINDING) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC) + .buffer_info(std::slice::from_ref(&point_light_info)), + ); } // SAFETY: Descriptor sets, sampler and image views are live; every texture upload completed its shader-read transition. unsafe { device.device().update_descriptor_sets(&writes, &[]) }; @@ -1114,6 +1253,12 @@ pub(super) fn destroy_swapchain_resources( device .device() .free_memory(resources.material_specular_buffer.memory, None); + device + .device() + .destroy_buffer(resources.point_light_buffer.buffer, None); + device + .device() + .free_memory(resources.point_light_buffer.memory, None); } } @@ -1177,5 +1322,9 @@ fn destroy_partial_swapchain_resources( device.device().destroy_buffer(buffer.buffer, None); device.device().free_memory(buffer.memory, None); } + if let Some(buffer) = partial.point_light_buffer { + device.device().destroy_buffer(buffer.buffer, None); + device.device().free_memory(buffer.memory, None); + } } } diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index b8a3d71..b06bb26 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -492,6 +492,51 @@ fn fragment_shader_dispatches_all_four_material_modes() { } #[test] +fn point_light_storage_payload_is_counted_and_packed_as_three_vec4s() { + let lights = [VulkanPointLight { + position: [1.0, 2.0, 3.0], + rgb: [4.0, 5.0, 6.0], + range: 7.0, + coefficients: [8.0, 9.0, 10.0], + active: true, + }]; + let bytes = super::resources::encode_point_lights(&lights).expect("point-light payload"); + assert_eq!( + bytes.len(), + VULKAN_POINT_LIGHT_HEADER_BYTES + VULKAN_POINT_LIGHT_RECORD_BYTES + ); + assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 1); + let values = (0..12) + .map(|index| { + let offset = VULKAN_POINT_LIGHT_HEADER_BYTES + index * std::mem::size_of::<f32>(); + f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) + }) + .collect::<Vec<_>>(); + assert_eq!( + values, + vec![1.0, 2.0, 3.0, 7.0, 4.0, 5.0, 6.0, 1.0, 8.0, 9.0, 10.0, 1.0] + ); + + let many = vec![VulkanPointLight::default(); 33]; + let bytes = super::resources::encode_point_lights(&many).expect("uncapped point-light payload"); + assert_eq!(u32::from_ne_bytes(bytes[0..4].try_into().unwrap()), 33); + assert_eq!( + bytes.len(), + VULKAN_POINT_LIGHT_HEADER_BYTES + 33 * VULKAN_POINT_LIGHT_RECORD_BYTES + ); +} + +#[test] +fn vertex_shader_sums_the_runtime_point_light_array() { + let vertex = include_str!("../../shaders/triangle.vert"); + assert!( + vertex.contains("layout(std430, set = 0, binding = 6) readonly buffer PointLightBuffer") + ); + assert!(vertex.contains("for (uint index = 0u; index < point_light_buffer.header.x; ++index)")); + assert!(!vertex.contains("if (point_rgb_active.w")); +} + +#[test] fn native_specular_reference_uses_squared_power_and_total_overflow_curve() { fn reference_power(cosine: f32, power: u8) -> f32 { if power == 0 { @@ -533,7 +578,7 @@ fn native_specular_reference_uses_squared_power_and_total_overflow_curve() { fn shader_computes_radial_fog_per_vertex_and_interpolates_it() { let vertex = include_str!("../../shaders/triangle.vert"); let fragment = include_str!("../../shaders/triangle.frag"); - assert!(vertex.contains("out_fog_factor = native_fog_factor(in_position);")); + assert!(vertex.contains("out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);")); assert!(fragment.contains("layout(location = 4) in float in_fog_factor;")); assert!(!fragment.contains("distance(in_world_position")); assert!(!fragment.contains("float fog_factor()")); |
