feat(render): upload dynamic point lights per frame
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
This commit is contained in:
8 files changed
+575
-55
No files matched your search
@@ -39,6 +39,17 @@ layout(std430, set = 0, binding = 5) readonly buffer MaterialSpecular {
|
|||||||
vec4 overlay;
|
vec4 overlay;
|
||||||
} material_specular;
|
} material_specular;
|
||||||
|
|
||||||
|
struct PointLightRecord {
|
||||||
|
vec4 position_range;
|
||||||
|
vec4 rgb_active;
|
||||||
|
vec4 coefficients_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(std430, set = 0, binding = 6) readonly buffer PointLightBuffer {
|
||||||
|
uvec4 header;
|
||||||
|
PointLightRecord records[];
|
||||||
|
} point_light_buffer;
|
||||||
|
|
||||||
layout(push_constant) uniform MaterialConstants {
|
layout(push_constant) uniform MaterialConstants {
|
||||||
layout(offset = 0) float alpha_cutoff;
|
layout(offset = 0) float alpha_cutoff;
|
||||||
layout(offset = 4) float diffuse_alpha;
|
layout(offset = 4) float diffuse_alpha;
|
||||||
@@ -180,29 +191,30 @@ LightingTerms native_material_lighting(
|
|||||||
1.0
|
1.0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
vec4 point_position_range = frame.point_position_range;
|
for (uint index = 0u; index < point_light_buffer.header.x; ++index) {
|
||||||
vec4 point_rgb_active = frame.point_rgb_active;
|
PointLightRecord point_light = point_light_buffer.records[index];
|
||||||
vec4 point_coefficients_active = frame.point_coefficients_active;
|
vec3 to_light = point_light.position_range.xyz - world_position;
|
||||||
vec3 to_light = point_position_range.xyz - world_position;
|
float distance_squared = dot(to_light, to_light);
|
||||||
float distance_squared = dot(to_light, to_light);
|
float range = point_light.position_range.w;
|
||||||
float range = point_position_range.w;
|
if (point_light.rgb_active.w <= 0.5 || range <= 0.0 || distance_squared > range * range) {
|
||||||
if (point_rgb_active.w > 0.5 && range > 0.0 && distance_squared <= range * range) {
|
continue;
|
||||||
|
}
|
||||||
float distance_to_light = sqrt(distance_squared);
|
float distance_to_light = sqrt(distance_squared);
|
||||||
if (distance_to_light > 0.000001) {
|
if (distance_to_light > 0.000001) {
|
||||||
vec3 light_direction = to_light / distance_to_light;
|
vec3 light_direction = to_light / distance_to_light;
|
||||||
float ndotl = dot(normal, light_direction);
|
float ndotl = dot(normal, light_direction);
|
||||||
if (ndotl > 0.0) {
|
if (ndotl > 0.0) {
|
||||||
float t = (range - distance_to_light) / range;
|
float t = (range - distance_to_light) / range;
|
||||||
float attenuation = point_coefficients_active.x
|
float attenuation = point_light.coefficients_active.x
|
||||||
+ point_coefficients_active.y * t
|
+ point_light.coefficients_active.y * t
|
||||||
+ point_coefficients_active.z * t * t;
|
+ point_light.coefficients_active.z * t * t;
|
||||||
attenuation = max(attenuation, 0.0);
|
attenuation = max(attenuation, 0.0);
|
||||||
accumulated += ndotl * directional_rgb * point_rgb_active.rgb * attenuation;
|
accumulated += ndotl * directional_rgb * point_light.rgb_active.rgb * attenuation;
|
||||||
specular += specular_from_light(
|
specular += specular_from_light(
|
||||||
normal,
|
normal,
|
||||||
view_direction,
|
view_direction,
|
||||||
light_direction,
|
light_direction,
|
||||||
point_rgb_active.rgb,
|
point_light.rgb_active.rgb,
|
||||||
specular_rgb,
|
specular_rgb,
|
||||||
specular_power,
|
specular_power,
|
||||||
attenuation
|
attenuation
|
||||||
|
|||||||
Binary file not shown.
@@ -51,12 +51,13 @@ use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
|||||||
use self::resources::{
|
use self::resources::{
|
||||||
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
||||||
create_frame_uniform_buffer, create_index_buffer, create_material_specular_buffer,
|
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_point_light_buffer, create_readback_buffer, create_static_mesh_index_buffer,
|
||||||
create_static_texture_image, create_vertex_buffer, destroy_allocated_buffer,
|
create_static_mesh_vertex_buffer, create_static_texture_image, create_vertex_buffer,
|
||||||
destroy_allocated_image, destroy_depth_attachment, readback_buffer_bytes,
|
destroy_allocated_buffer, destroy_allocated_image, destroy_depth_attachment,
|
||||||
update_static_texture_image, write_frame_uniform, write_material_specular_buffer,
|
readback_buffer_bytes, update_static_texture_image, write_frame_uniform,
|
||||||
write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer,
|
write_material_specular_buffer, write_point_light_buffer, write_static_mesh_index_buffer,
|
||||||
VulkanAllocatedImage, VulkanDepthAttachment, VulkanFrameSync,
|
write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, VulkanAllocatedImage,
|
||||||
|
VulkanDepthAttachment, VulkanFrameSync,
|
||||||
};
|
};
|
||||||
pub use self::runtime::{
|
pub use self::runtime::{
|
||||||
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
@@ -69,8 +70,9 @@ pub use self::smoke_types::{
|
|||||||
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
||||||
VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh,
|
VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh,
|
||||||
VulkanStaticTexture, VulkanStaticTextureMip, VulkanStaticVertex, VulkanValidationReport,
|
VulkanStaticTexture, VulkanStaticTextureMip, VulkanStaticVertex, VulkanValidationReport,
|
||||||
VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_UNIFORM_BYTES,
|
VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_SLOT_COUNT, VULKAN_FRAME_UNIFORM_BYTES,
|
||||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES, VULKAN_POINT_LIGHT_HEADER_BYTES,
|
||||||
|
VULKAN_POINT_LIGHT_RECORD_BYTES,
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use self::surface::extension_name;
|
use self::surface::extension_name;
|
||||||
@@ -83,8 +85,8 @@ pub use self::swapchain::{
|
|||||||
VulkanSwapchainProbeError, VulkanSwapchainReport,
|
VulkanSwapchainProbeError, VulkanSwapchainReport,
|
||||||
};
|
};
|
||||||
use self::swapchain_resources::{
|
use self::swapchain_resources::{
|
||||||
create_swapchain_resources, destroy_swapchain_resources, update_material_texture_descriptor,
|
create_swapchain_resources, destroy_swapchain_resources, ensure_point_light_capacity,
|
||||||
VulkanSwapchainResources,
|
update_material_texture_descriptor, VulkanSwapchainResources,
|
||||||
};
|
};
|
||||||
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
|
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use ash::vk;
|
|||||||
use fparkan_platform::DepthStencilSupport;
|
use fparkan_platform::DepthStencilSupport;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh,
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanPointLight, VulkanSmokeRendererError,
|
||||||
VulkanStaticTexture,
|
VulkanStaticMesh, VulkanStaticTexture,
|
||||||
};
|
};
|
||||||
use crate::select_depth_stencil_attachment_format;
|
use crate::select_depth_stencil_attachment_format;
|
||||||
|
|
||||||
@@ -520,7 +520,6 @@ pub(super) fn create_frame_uniform_buffer(
|
|||||||
instance: &VulkanInstanceProbe,
|
instance: &VulkanInstanceProbe,
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> {
|
) -> Result<(VulkanAllocatedBuffer, u64), VulkanSmokeRendererError> {
|
||||||
const FRAME_SLOTS: u64 = 2;
|
|
||||||
const FRAME_BYTES: u64 = super::VULKAN_FRAME_UNIFORM_BYTES as u64;
|
const FRAME_BYTES: u64 = super::VULKAN_FRAME_UNIFORM_BYTES as u64;
|
||||||
let limits =
|
let limits =
|
||||||
// SAFETY: The physical device belongs to this live instance and the query returns a value copy.
|
// 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
|
||||||
* alignment;
|
* alignment;
|
||||||
let size =
|
let size = stride
|
||||||
stride
|
.checked_mul(super::VULKAN_FRAME_SLOT_COUNT as u64)
|
||||||
.checked_mul(FRAME_SLOTS)
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
context: "frame uniform buffer size overflow",
|
||||||
context: "frame uniform buffer size overflow",
|
})?;
|
||||||
})?;
|
|
||||||
let bytes = vec![
|
let bytes = vec![
|
||||||
0_u8;
|
0_u8;
|
||||||
usize::try_from(size).map_err(|_| {
|
usize::try_from(size).map_err(|_| {
|
||||||
@@ -556,6 +554,192 @@ pub(super) fn create_frame_uniform_buffer(
|
|||||||
Ok((buffer, stride))
|
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.
|
/// Allocates one aligned storage-buffer record for every material descriptor.
|
||||||
///
|
///
|
||||||
/// The descriptor uses a dynamic storage-buffer offset. Keeping the stride
|
/// The descriptor uses a dynamic storage-buffer offset. Keeping the stride
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use super::{
|
|||||||
create_validation_messenger, create_vertex_buffer, create_vulkan_instance_probe,
|
create_validation_messenger, create_vertex_buffer, create_vulkan_instance_probe,
|
||||||
create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe,
|
create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe,
|
||||||
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image,
|
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image,
|
||||||
destroy_swapchain_resources, plan_vulkan_surface, readback_buffer_bytes,
|
destroy_swapchain_resources, ensure_point_light_capacity, plan_vulkan_surface,
|
||||||
update_static_texture_image, write_frame_uniform, write_material_specular_buffer,
|
readback_buffer_bytes, update_static_texture_image, write_frame_uniform,
|
||||||
write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer,
|
write_material_specular_buffer, write_point_light_buffer, write_static_mesh_index_buffer,
|
||||||
VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanReadbackArtifact,
|
write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, VulkanInstanceConfig,
|
||||||
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanReadbackArtifact, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
|
||||||
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
|
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe,
|
||||||
VulkanValidationReport,
|
VulkanSwapchainResources, VulkanValidationMessenger, VulkanValidationReport,
|
||||||
};
|
};
|
||||||
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
||||||
use crate::shader::validate_triangle_shaders;
|
use crate::shader::validate_triangle_shaders;
|
||||||
@@ -440,6 +440,8 @@ impl VulkanSmokeRenderer {
|
|||||||
readback_enabled: false,
|
readback_enabled: false,
|
||||||
camera: create_info.camera,
|
camera: create_info.camera,
|
||||||
frame_uniforms: super::VulkanFrameUniforms::from_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(),
|
frame_sync: Vec::new(),
|
||||||
images_in_flight: Vec::new(),
|
images_in_flight: Vec::new(),
|
||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
@@ -576,6 +578,41 @@ impl VulkanSmokeRenderer {
|
|||||||
Ok(())
|
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
|
/// Enables the optional swapchain image copy used by screenshot/readback
|
||||||
/// callers. Runtime frames leave it disabled to avoid a transfer every
|
/// callers. Runtime frames leave it disabled to avoid a transfer every
|
||||||
/// frame.
|
/// frame.
|
||||||
@@ -1318,6 +1355,44 @@ impl VulkanSmokeRenderer {
|
|||||||
context: "vkWaitForFences",
|
context: "vkWaitForFences",
|
||||||
result: error,
|
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 {
|
if self.material_specular_dirty {
|
||||||
// The storage buffer is shared by all in-flight frames. Wait for
|
// The storage buffer is shared by all in-flight frames. Wait for
|
||||||
// every prior use before replacing the records so a setter cannot
|
// every prior use before replacing the records so a setter cannot
|
||||||
@@ -1377,14 +1452,42 @@ impl VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
self.images_in_flight[image_index_usize] = in_flight_fence;
|
self.images_in_flight[image_index_usize] = in_flight_fence;
|
||||||
let frame_bytes = self.frame_uniforms.to_ne_bytes();
|
let frame_bytes = self.frame_uniforms.to_ne_bytes();
|
||||||
let resources = self.resources_ref()?;
|
{
|
||||||
write_frame_uniform(
|
let explicit_point_lights = self.point_lights_explicit;
|
||||||
self.device_ref()?,
|
let legacy_point_light = self.frame_uniforms.point_light;
|
||||||
&resources.frame_uniform_buffer,
|
let point_lights = if explicit_point_lights {
|
||||||
resources.frame_uniform_stride,
|
self.point_lights.as_slice()
|
||||||
self.current_frame,
|
} else if legacy_point_light.active {
|
||||||
&frame_bytes,
|
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
|
let render_finished = self
|
||||||
.resources_ref()?
|
.resources_ref()?
|
||||||
.render_finished
|
.render_finished
|
||||||
@@ -1693,6 +1796,12 @@ impl VulkanSmokeRenderer {
|
|||||||
.saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
|
.saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
|
||||||
)
|
)
|
||||||
.unwrap_or(u32::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(
|
let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
|
||||||
VulkanSmokeRendererError::InvariantViolation {
|
VulkanSmokeRendererError::InvariantViolation {
|
||||||
context: "static material descriptor set",
|
context: "static material descriptor set",
|
||||||
@@ -1704,7 +1813,7 @@ impl VulkanSmokeRenderer {
|
|||||||
resources.pipeline_layout,
|
resources.pipeline_layout,
|
||||||
0,
|
0,
|
||||||
std::slice::from_ref(descriptor_set),
|
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();
|
let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
|
||||||
device.device().cmd_push_constants(
|
device.device().cmd_push_constants(
|
||||||
|
|||||||
@@ -194,6 +194,15 @@ impl Default for VulkanPointLight {
|
|||||||
/// state.
|
/// state.
|
||||||
pub const VULKAN_FRAME_UNIFORM_BYTES: usize = 368;
|
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.
|
/// Bytes occupied by one material's base and overlay specular records.
|
||||||
///
|
///
|
||||||
/// Each record is one `vec4`: RGB followed by the native integer power stored
|
/// Each record is one `vec4`: RGB followed by the native integer power stored
|
||||||
@@ -1359,6 +1368,11 @@ pub enum VulkanSmokeRendererError {
|
|||||||
/// Validation failure detail.
|
/// Validation failure detail.
|
||||||
context: &'static str,
|
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.
|
/// The submitted static texture cannot be represented by this path.
|
||||||
InvalidStaticTexture {
|
InvalidStaticTexture {
|
||||||
/// Validation failure detail.
|
/// Validation failure detail.
|
||||||
@@ -1389,6 +1403,7 @@ impl std::fmt::Display for VulkanSmokeRendererError {
|
|||||||
Self::InvalidStaticCamera { context } => {
|
Self::InvalidStaticCamera { context } => {
|
||||||
write!(f, "invalid static camera: {context}")
|
write!(f, "invalid static camera: {context}")
|
||||||
}
|
}
|
||||||
|
Self::InvalidPointLights { context } => write!(f, "invalid point lights: {context}"),
|
||||||
Self::InvalidStaticTexture { context } => {
|
Self::InvalidStaticTexture { context } => {
|
||||||
write!(f, "invalid static texture: {context}")
|
write!(f, "invalid static texture: {context}")
|
||||||
}
|
}
|
||||||
@@ -1454,6 +1469,10 @@ pub struct VulkanSmokeRenderer {
|
|||||||
pub(super) readback_enabled: bool,
|
pub(super) readback_enabled: bool,
|
||||||
pub(super) camera: VulkanStaticCamera,
|
pub(super) camera: VulkanStaticCamera,
|
||||||
pub(super) frame_uniforms: VulkanFrameUniforms,
|
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) frame_sync: Vec<VulkanFrameSync>,
|
||||||
pub(super) images_in_flight: Vec<vk::Fence>,
|
pub(super) images_in_flight: Vec<vk::Fence>,
|
||||||
pub(super) current_frame: usize,
|
pub(super) current_frame: usize,
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
color_subresource_range, create_depth_attachment, create_material_specular_buffer,
|
color_subresource_range, create_depth_attachment, create_material_specular_buffer,
|
||||||
destroy_depth_attachment, VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment,
|
create_point_light_buffer, destroy_depth_attachment, VulkanAllocatedBuffer,
|
||||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe,
|
VulkanAllocatedImage, VulkanDepthAttachment, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
|
||||||
TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES,
|
VulkanSmokeRendererError, VulkanSwapchainProbe, TRIANGLE_FRAGMENT_SHADER_WORDS,
|
||||||
|
TRIANGLE_VERTEX_SHADER_WORDS, VULKAN_FRAME_UNIFORM_BYTES,
|
||||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,6 +34,11 @@ pub(super) struct VulkanSwapchainResources {
|
|||||||
pub(super) frame_uniform_stride: u64,
|
pub(super) frame_uniform_stride: u64,
|
||||||
pub(super) material_specular_buffer: VulkanAllocatedBuffer,
|
pub(super) material_specular_buffer: VulkanAllocatedBuffer,
|
||||||
pub(super) material_specular_stride: u64,
|
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.
|
/// Live graphics pipelines indexed by canonical backend-neutral state.
|
||||||
pub(super) pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
pub(super) pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
||||||
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
||||||
@@ -73,6 +79,71 @@ pub(super) fn update_material_texture_descriptor(
|
|||||||
Ok(())
|
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 {
|
struct PartialSwapchainResources {
|
||||||
image_views: Vec<vk::ImageView>,
|
image_views: Vec<vk::ImageView>,
|
||||||
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||||
@@ -87,6 +158,11 @@ struct PartialSwapchainResources {
|
|||||||
frame_uniform_stride: u64,
|
frame_uniform_stride: u64,
|
||||||
material_specular_buffer: Option<VulkanAllocatedBuffer>,
|
material_specular_buffer: Option<VulkanAllocatedBuffer>,
|
||||||
material_specular_stride: u64,
|
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>,
|
pipelines: BTreeMap<PipelineKey, vk::Pipeline>,
|
||||||
framebuffers: Vec<vk::Framebuffer>,
|
framebuffers: Vec<vk::Framebuffer>,
|
||||||
command_buffers: Vec<vk::CommandBuffer>,
|
command_buffers: Vec<vk::CommandBuffer>,
|
||||||
@@ -133,6 +209,11 @@ pub(super) fn create_swapchain_resources(
|
|||||||
frame_uniform_stride: 0,
|
frame_uniform_stride: 0,
|
||||||
material_specular_buffer: None,
|
material_specular_buffer: None,
|
||||||
material_specular_stride: 0,
|
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(),
|
pipelines: BTreeMap::new(),
|
||||||
framebuffers: Vec::new(),
|
framebuffers: Vec::new(),
|
||||||
command_buffers: 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_stride = material_specular_stride;
|
||||||
partial.material_specular_buffer = Some(material_specular_buffer);
|
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 (
|
let (
|
||||||
depth_attachment,
|
depth_attachment,
|
||||||
render_pass,
|
render_pass,
|
||||||
@@ -199,6 +294,11 @@ pub(super) fn create_swapchain_resources(
|
|||||||
.material_specular_buffer
|
.material_specular_buffer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("material specular buffer is present before descriptor creation"),
|
.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,
|
Ok(bundle) => bundle,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -320,6 +420,13 @@ pub(super) fn create_swapchain_resources(
|
|||||||
.material_specular_buffer
|
.material_specular_buffer
|
||||||
.expect("material specular buffer is present after successful swapchain setup"),
|
.expect("material specular buffer is present after successful swapchain setup"),
|
||||||
material_specular_stride: partial.material_specular_stride,
|
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,
|
pipelines: partial.pipelines,
|
||||||
framebuffers: partial.framebuffers,
|
framebuffers: partial.framebuffers,
|
||||||
command_buffers: partial.command_buffers,
|
command_buffers: partial.command_buffers,
|
||||||
@@ -349,6 +456,8 @@ fn create_swapchain_pipeline_bundle(
|
|||||||
depth_request: DepthStencilSupport,
|
depth_request: DepthStencilSupport,
|
||||||
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
||||||
material_specular_buffer: &VulkanAllocatedBuffer,
|
material_specular_buffer: &VulkanAllocatedBuffer,
|
||||||
|
point_light_buffer: &VulkanAllocatedBuffer,
|
||||||
|
point_light_range: u64,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
VulkanDepthAttachment,
|
VulkanDepthAttachment,
|
||||||
@@ -377,6 +486,8 @@ fn create_swapchain_pipeline_bundle(
|
|||||||
texture_slot_indices,
|
texture_slot_indices,
|
||||||
frame_uniform_buffer,
|
frame_uniform_buffer,
|
||||||
material_specular_buffer,
|
material_specular_buffer,
|
||||||
|
point_light_buffer,
|
||||||
|
point_light_range,
|
||||||
)
|
)
|
||||||
.inspect_err(|_| {
|
.inspect_err(|_| {
|
||||||
// SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure.
|
// 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],
|
texture_slot_indices: &[usize],
|
||||||
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
frame_uniform_buffer: &VulkanAllocatedBuffer,
|
||||||
material_specular_buffer: &VulkanAllocatedBuffer,
|
material_specular_buffer: &VulkanAllocatedBuffer,
|
||||||
|
point_light_buffer: &VulkanAllocatedBuffer,
|
||||||
|
point_light_range: u64,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
vk::DescriptorSetLayout,
|
vk::DescriptorSetLayout,
|
||||||
@@ -615,6 +728,7 @@ fn create_texture_descriptor_bundle(
|
|||||||
const TEXTURES_PER_MATERIAL: usize = 4;
|
const TEXTURES_PER_MATERIAL: usize = 4;
|
||||||
const FRAME_BINDING: u32 = 4;
|
const FRAME_BINDING: u32 = 4;
|
||||||
const MATERIAL_SPECULAR_BINDING: u32 = 5;
|
const MATERIAL_SPECULAR_BINDING: u32 = 5;
|
||||||
|
const POINT_LIGHT_BINDING: u32 = 6;
|
||||||
let mut bindings = (0..TEXTURES_PER_MATERIAL)
|
let mut bindings = (0..TEXTURES_PER_MATERIAL)
|
||||||
.map(|binding| {
|
.map(|binding| {
|
||||||
vk::DescriptorSetLayoutBinding::default()
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
@@ -638,6 +752,13 @@ fn create_texture_descriptor_bundle(
|
|||||||
.descriptor_count(1)
|
.descriptor_count(1)
|
||||||
.stage_flags(vk::ShaderStageFlags::VERTEX),
|
.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);
|
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
|
||||||
// SAFETY: The layout description is stack-owned and references no external memory.
|
// SAFETY: The layout description is stack-owned and references no external memory.
|
||||||
let layout = unsafe {
|
let layout = unsafe {
|
||||||
@@ -676,10 +797,17 @@ fn create_texture_descriptor_bundle(
|
|||||||
let frame_pool_size = vk::DescriptorPoolSize::default()
|
let frame_pool_size = vk::DescriptorPoolSize::default()
|
||||||
.ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
.ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
|
||||||
.descriptor_count(material_count);
|
.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)
|
.ty(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
||||||
.descriptor_count(material_count);
|
.descriptor_count(material_count.checked_mul(2).ok_or_else(|| {
|
||||||
let pool_sizes = [pool_size, frame_pool_size, material_specular_pool_size];
|
// 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()
|
let pool_info = vk::DescriptorPoolCreateInfo::default()
|
||||||
.pool_sizes(&pool_sizes)
|
.pool_sizes(&pool_sizes)
|
||||||
.max_sets(material_count);
|
.max_sets(material_count);
|
||||||
@@ -751,7 +879,11 @@ fn create_texture_descriptor_bundle(
|
|||||||
.buffer(material_specular_buffer.buffer)
|
.buffer(material_specular_buffer.buffer)
|
||||||
.offset(0)
|
.offset(0)
|
||||||
.range(VULKAN_MATERIAL_SPECULAR_RECORD_BYTES as u64);
|
.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 (set_index, descriptor_set) in descriptor_sets.iter().copied().enumerate() {
|
||||||
for binding in 0..TEXTURES_PER_MATERIAL {
|
for binding in 0..TEXTURES_PER_MATERIAL {
|
||||||
let image_info = &image_infos[set_index * TEXTURES_PER_MATERIAL + binding];
|
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)
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
|
||||||
.buffer_info(std::slice::from_ref(&material_specular_info)),
|
.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.
|
// SAFETY: Descriptor sets, sampler and image views are live; every texture upload completed its shader-read transition.
|
||||||
unsafe { device.device().update_descriptor_sets(&writes, &[]) };
|
unsafe { device.device().update_descriptor_sets(&writes, &[]) };
|
||||||
@@ -1114,6 +1253,12 @@ pub(super) fn destroy_swapchain_resources(
|
|||||||
device
|
device
|
||||||
.device()
|
.device()
|
||||||
.free_memory(resources.material_specular_buffer.memory, None);
|
.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().destroy_buffer(buffer.buffer, None);
|
||||||
device.device().free_memory(buffer.memory, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,6 +491,51 @@ fn fragment_shader_dispatches_all_four_material_modes() {
|
|||||||
assert!(!fragment.contains("material_mode > 2.5"));
|
assert!(!fragment.contains("material_mode > 2.5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
|
fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
|
||||||
fn reference_power(cosine: f32, power: u8) -> f32 {
|
fn reference_power(cosine: f32, power: u8) -> f32 {
|
||||||
|
|||||||
Reference in new issue
Block a user