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;
|
||||
} 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(offset = 0) float alpha_cutoff;
|
||||
layout(offset = 4) float diffuse_alpha;
|
||||
@@ -180,29 +191,30 @@ LightingTerms native_material_lighting(
|
||||
1.0
|
||||
);
|
||||
}
|
||||
vec4 point_position_range = frame.point_position_range;
|
||||
vec4 point_rgb_active = frame.point_rgb_active;
|
||||
vec4 point_coefficients_active = frame.point_coefficients_active;
|
||||
vec3 to_light = point_position_range.xyz - world_position;
|
||||
float distance_squared = dot(to_light, to_light);
|
||||
float range = point_position_range.w;
|
||||
if (point_rgb_active.w > 0.5 && range > 0.0 && distance_squared <= range * range) {
|
||||
for (uint index = 0u; index < point_light_buffer.header.x; ++index) {
|
||||
PointLightRecord point_light = point_light_buffer.records[index];
|
||||
vec3 to_light = point_light.position_range.xyz - world_position;
|
||||
float distance_squared = dot(to_light, to_light);
|
||||
float range = point_light.position_range.w;
|
||||
if (point_light.rgb_active.w <= 0.5 || range <= 0.0 || distance_squared > range * range) {
|
||||
continue;
|
||||
}
|
||||
float distance_to_light = sqrt(distance_squared);
|
||||
if (distance_to_light > 0.000001) {
|
||||
vec3 light_direction = to_light / distance_to_light;
|
||||
float ndotl = dot(normal, light_direction);
|
||||
if (ndotl > 0.0) {
|
||||
float t = (range - distance_to_light) / range;
|
||||
float attenuation = point_coefficients_active.x
|
||||
+ point_coefficients_active.y * t
|
||||
+ point_coefficients_active.z * t * t;
|
||||
float attenuation = point_light.coefficients_active.x
|
||||
+ point_light.coefficients_active.y * t
|
||||
+ point_light.coefficients_active.z * t * t;
|
||||
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(
|
||||
normal,
|
||||
view_direction,
|
||||
light_direction,
|
||||
point_rgb_active.rgb,
|
||||
point_light.rgb_active.rgb,
|
||||
specular_rgb,
|
||||
specular_power,
|
||||
attenuation
|
||||
|
||||
Binary file not shown.
@@ -51,12 +51,13 @@ use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||
use self::resources::{
|
||||
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
||||
create_frame_uniform_buffer, create_index_buffer, create_material_specular_buffer,
|
||||
create_readback_buffer, create_static_mesh_index_buffer, create_static_mesh_vertex_buffer,
|
||||
create_static_texture_image, create_vertex_buffer, destroy_allocated_buffer,
|
||||
destroy_allocated_image, destroy_depth_attachment, readback_buffer_bytes,
|
||||
update_static_texture_image, write_frame_uniform, write_material_specular_buffer,
|
||||
write_static_mesh_index_buffer, write_static_mesh_vertex_buffer, VulkanAllocatedBuffer,
|
||||
VulkanAllocatedImage, VulkanDepthAttachment, VulkanFrameSync,
|
||||
create_point_light_buffer, create_readback_buffer, create_static_mesh_index_buffer,
|
||||
create_static_mesh_vertex_buffer, create_static_texture_image, create_vertex_buffer,
|
||||
destroy_allocated_buffer, destroy_allocated_image, destroy_depth_attachment,
|
||||
readback_buffer_bytes, update_static_texture_image, write_frame_uniform,
|
||||
write_material_specular_buffer, write_point_light_buffer, write_static_mesh_index_buffer,
|
||||
write_static_mesh_vertex_buffer, VulkanAllocatedBuffer, VulkanAllocatedImage,
|
||||
VulkanDepthAttachment, VulkanFrameSync,
|
||||
};
|
||||
pub use self::runtime::{
|
||||
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||
@@ -69,8 +70,9 @@ pub use self::smoke_types::{
|
||||
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
||||
VulkanStaticCamera, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh,
|
||||
VulkanStaticTexture, VulkanStaticTextureMip, VulkanStaticVertex, VulkanValidationReport,
|
||||
VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_UNIFORM_BYTES,
|
||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES,
|
||||
VULKAN_DIRECTIONAL_LIGHT_COUNT, VULKAN_FRAME_SLOT_COUNT, VULKAN_FRAME_UNIFORM_BYTES,
|
||||
VULKAN_MATERIAL_SPECULAR_RECORD_BYTES, VULKAN_POINT_LIGHT_HEADER_BYTES,
|
||||
VULKAN_POINT_LIGHT_RECORD_BYTES,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use self::surface::extension_name;
|
||||
@@ -83,8 +85,8 @@ pub use self::swapchain::{
|
||||
VulkanSwapchainProbeError, VulkanSwapchainReport,
|
||||
};
|
||||
use self::swapchain_resources::{
|
||||
create_swapchain_resources, destroy_swapchain_resources, update_material_texture_descriptor,
|
||||
VulkanSwapchainResources,
|
||||
create_swapchain_resources, destroy_swapchain_resources, ensure_point_light_capacity,
|
||||
update_material_texture_descriptor, VulkanSwapchainResources,
|
||||
};
|
||||
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
|
||||
use ash::vk;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -440,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,
|
||||
@@ -576,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.
|
||||
@@ -1318,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
|
||||
@@ -1377,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
|
||||
@@ -1693,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",
|
||||
@@ -1704,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(
|
||||
|
||||
@@ -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
|
||||
@@ -1359,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.
|
||||
@@ -1389,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}")
|
||||
}
|
||||
@@ -1454,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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,6 +491,51 @@ fn fragment_shader_dispatches_all_four_material_modes() {
|
||||
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]
|
||||
fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
|
||||
fn reference_power(cosine: f32, power: u8) -> f32 {
|
||||
|
||||
Reference in new issue
Block a user