diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-18 10:23:35 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-18 10:24:16 +0300 |
| commit | 5a48d0b26685526eaa2c7c824e6e0828177246b0 (patch) | |
| tree | 3cbeb39f69ea27b50216389b585678d7c7daa163 /adapters/fparkan-render-vulkan/src/ffi/resources.rs | |
| parent | 7abf45de7e3356362c55f966c446291728723f59 (diff) | |
| download | fparkan-codex/runtime-point-light-ssbo.tar.xz fparkan-codex/runtime-point-light-ssbo.zip | |
feat(render): upload dynamic point lights per framecodex/runtime-point-light-ssbo
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/resources.rs')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 202 |
1 files changed, 193 insertions, 9 deletions
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 |
