aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs202
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs158
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs57
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs163
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs47
5 files changed, 580 insertions, 47 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
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index c9de5ba..b3d5ab3 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.
@@ -1329,6 +1355,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 +1452,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 +1796,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 +1813,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(
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index f628e23..8abc53b 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
@@ -505,6 +514,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.
@@ -1028,6 +1064,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 +1091,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])
@@ -1321,6 +1368,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 +1403,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 +1469,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()"));