perf(render): retain static mesh prefixes between frames
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
This commit is contained in:
5 files changed
+177
-71
No files matched your search
@@ -243,18 +243,25 @@ pub(super) fn create_vertex_buffer(
|
|||||||
|
|
||||||
/// Replaces the bytes of a host-visible static vertex allocation.
|
/// Replaces the bytes of a host-visible static vertex allocation.
|
||||||
///
|
///
|
||||||
/// The live renderer waits for the device before calling this helper, so a
|
/// The live renderer waits for submitted readers before calling this helper, so a
|
||||||
/// single shared allocation can safely carry camera-relative sky and weather
|
/// single shared allocation can safely carry camera-relative sky and weather
|
||||||
/// geometry between frames.
|
/// geometry between frames.
|
||||||
pub(super) fn write_static_mesh_vertex_buffer(
|
pub(super) fn write_static_mesh_vertex_buffer(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
buffer: &VulkanAllocatedBuffer,
|
buffer: &VulkanAllocatedBuffer,
|
||||||
|
first_vertex: usize,
|
||||||
vertices: &[super::VulkanStaticVertex],
|
vertices: &[super::VulkanStaticVertex],
|
||||||
) -> Result<(), VulkanSmokeRendererError> {
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
let bytes = encode_static_vertices(vertices)?;
|
let bytes = encode_static_vertices(vertices)?;
|
||||||
|
let offset = first_vertex
|
||||||
|
.checked_mul(14 * std::mem::size_of::<f32>())
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "static mesh vertex upload offset overflows address space",
|
||||||
|
})?;
|
||||||
write_mapped_buffer(
|
write_mapped_buffer(
|
||||||
device,
|
device,
|
||||||
buffer,
|
buffer,
|
||||||
|
offset,
|
||||||
&bytes,
|
&bytes,
|
||||||
"vkMapMemory(static mesh vertex update)",
|
"vkMapMemory(static mesh vertex update)",
|
||||||
)
|
)
|
||||||
@@ -295,29 +302,28 @@ fn encode_static_vertices(
|
|||||||
fn write_mapped_buffer(
|
fn write_mapped_buffer(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
buffer: &VulkanAllocatedBuffer,
|
buffer: &VulkanAllocatedBuffer,
|
||||||
|
offset: usize,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
) -> Result<(), VulkanSmokeRendererError> {
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
let byte_len =
|
if bytes.is_empty() {
|
||||||
u64::try_from(bytes.len()).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
|
return Ok(());
|
||||||
context: "static mesh upload exceeds address space",
|
|
||||||
})?;
|
|
||||||
if byte_len == 0 {
|
|
||||||
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
|
||||||
context: "static mesh upload cannot be empty",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// SAFETY: The caller validates that the allocation is large enough and
|
// SAFETY: The caller validates that the allocation is large enough and
|
||||||
// waits for all queue work that could read it before this write.
|
// waits for all queue work that could read it before this write.
|
||||||
let mapped = unsafe {
|
let mapped = unsafe {
|
||||||
device
|
device.device().map_memory(
|
||||||
.device()
|
buffer.memory,
|
||||||
.map_memory(buffer.memory, 0, byte_len, vk::MemoryMapFlags::empty())
|
0,
|
||||||
|
vk::WHOLE_SIZE,
|
||||||
|
vk::MemoryMapFlags::empty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.map_err(|result| VulkanSmokeRendererError::VulkanOperation { context, result })?;
|
.map_err(|result| VulkanSmokeRendererError::VulkanOperation { context, result })?;
|
||||||
// SAFETY: `mapped` covers exactly the byte slice copied below.
|
// SAFETY: The caller bounds the upload against the allocation. Mapping
|
||||||
|
// from zero also meets minMemoryMapAlignment for arbitrary vertex offsets.
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
|
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>().add(offset), bytes.len());
|
||||||
device.device().unmap_memory(buffer.memory);
|
device.device().unmap_memory(buffer.memory);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -351,12 +357,19 @@ pub(super) fn create_index_buffer(
|
|||||||
pub(super) fn write_static_mesh_index_buffer(
|
pub(super) fn write_static_mesh_index_buffer(
|
||||||
device: &VulkanLogicalDeviceProbe,
|
device: &VulkanLogicalDeviceProbe,
|
||||||
buffer: &VulkanAllocatedBuffer,
|
buffer: &VulkanAllocatedBuffer,
|
||||||
|
first_index: usize,
|
||||||
indices: &[u32],
|
indices: &[u32],
|
||||||
) -> Result<(), VulkanSmokeRendererError> {
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
let bytes = encode_static_indices(indices)?;
|
let bytes = encode_static_indices(indices)?;
|
||||||
|
let offset = first_index.checked_mul(std::mem::size_of::<u32>()).ok_or(
|
||||||
|
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "static mesh index upload offset overflows address space",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
write_mapped_buffer(
|
write_mapped_buffer(
|
||||||
device,
|
device,
|
||||||
buffer,
|
buffer,
|
||||||
|
offset,
|
||||||
&bytes,
|
&bytes,
|
||||||
"vkMapMemory(static mesh index update)",
|
"vkMapMemory(static mesh index update)",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -464,6 +464,9 @@ impl VulkanSmokeRenderer {
|
|||||||
readback_copy_count: 0,
|
readback_copy_count: 0,
|
||||||
readback_byte_count: 0,
|
readback_byte_count: 0,
|
||||||
readback_fnv1a64: 0,
|
readback_fnv1a64: 0,
|
||||||
|
mesh_vertex_upload_bytes: 0,
|
||||||
|
mesh_index_upload_bytes: 0,
|
||||||
|
mesh_full_upload_bytes: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
renderer.rebuild_swapchain_resources(false)?;
|
renderer.rebuild_swapchain_resources(false)?;
|
||||||
@@ -495,6 +498,9 @@ impl VulkanSmokeRenderer {
|
|||||||
readback_copy_count: 0,
|
readback_copy_count: 0,
|
||||||
readback_byte_count: 0,
|
readback_byte_count: 0,
|
||||||
readback_fnv1a64: 0,
|
readback_fnv1a64: 0,
|
||||||
|
mesh_vertex_upload_bytes: 0,
|
||||||
|
mesh_index_upload_bytes: 0,
|
||||||
|
mesh_full_upload_bytes: 0,
|
||||||
};
|
};
|
||||||
Ok(renderer)
|
Ok(renderer)
|
||||||
}
|
}
|
||||||
@@ -628,7 +634,7 @@ impl VulkanSmokeRenderer {
|
|||||||
///
|
///
|
||||||
/// The initial mesh owns enough vertices for the scene's dynamic slots;
|
/// The initial mesh owns enough vertices for the scene's dynamic slots;
|
||||||
/// callers update those slots in place as camera-relative environment
|
/// callers update those slots in place as camera-relative environment
|
||||||
/// geometry moves. The device is idled before the host-visible write so
|
/// geometry moves. Submitted readers finish before the host-visible write so
|
||||||
/// no submitted command can observe a partial update.
|
/// no submitted command can observe a partial update.
|
||||||
pub fn update_vertices(
|
pub fn update_vertices(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -639,15 +645,8 @@ impl VulkanSmokeRenderer {
|
|||||||
context: "updated static mesh vertex count differs from allocation",
|
context: "updated static mesh vertex count differs from allocation",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// SAFETY: The renderer owns the queue and all submitted work; waiting
|
self.wait_for_submitted_frames()?;
|
||||||
// for idle makes the shared vertex allocation quiescent.
|
write_static_mesh_vertex_buffer(self.device_ref()?, self.vertex_buffer_ref()?, 0, vertices)
|
||||||
unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
|
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
|
||||||
context: "vkDeviceWaitIdle(static mesh vertex update)",
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
write_static_mesh_vertex_buffer(self.device_ref()?, self.vertex_buffer_ref()?, vertices)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads the current shared mesh while retaining dynamic draw state.
|
/// Uploads the current shared mesh while retaining dynamic draw state.
|
||||||
@@ -658,6 +657,9 @@ impl VulkanSmokeRenderer {
|
|||||||
/// transparency classification, and active counts owned by other setters
|
/// transparency classification, and active counts owned by other setters
|
||||||
/// remain untouched. The allocations grow to the largest submitted frame
|
/// remain untouched. The allocations grow to the largest submitted frame
|
||||||
/// and never grow for a frame that fits the retained capacity.
|
/// and never grow for a frame that fits the retained capacity.
|
||||||
|
/// `unchanged_vertices` and `unchanged_indices` retain prefixes already
|
||||||
|
/// uploaded by the caller; only the remaining suffixes are written. A
|
||||||
|
/// growing allocation receives the full slice, including its prefix.
|
||||||
///
|
///
|
||||||
/// A range with `index_count == 0` is valid and disables that range for the
|
/// A range with `index_count == 0` is valid and disables that range for the
|
||||||
/// frame. The mesh itself still needs at least one vertex and one index so
|
/// frame. The mesh itself still needs at least one vertex and one index so
|
||||||
@@ -667,6 +669,8 @@ impl VulkanSmokeRenderer {
|
|||||||
vertices: &[super::VulkanStaticVertex],
|
vertices: &[super::VulkanStaticVertex],
|
||||||
indices: &[u32],
|
indices: &[u32],
|
||||||
geometry_ranges: &[super::VulkanDynamicDrawRange],
|
geometry_ranges: &[super::VulkanDynamicDrawRange],
|
||||||
|
unchanged_vertices: usize,
|
||||||
|
unchanged_indices: usize,
|
||||||
) -> Result<(), VulkanSmokeRendererError> {
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
validate_geometry_upload(
|
validate_geometry_upload(
|
||||||
vertices,
|
vertices,
|
||||||
@@ -676,19 +680,17 @@ impl VulkanSmokeRenderer {
|
|||||||
self.vertex_count,
|
self.vertex_count,
|
||||||
self.index_capacity,
|
self.index_capacity,
|
||||||
)?;
|
)?;
|
||||||
|
let vertex_upload =
|
||||||
|
geometry_upload_range(vertices.len(), unchanged_vertices, self.vertex_count)?;
|
||||||
|
let index_upload =
|
||||||
|
geometry_upload_range(indices.len(), unchanged_indices, self.index_capacity)?;
|
||||||
|
|
||||||
let vertex_grew = vertices.len() > self.vertex_count;
|
let vertex_grew = vertices.len() > self.vertex_count;
|
||||||
let index_grew = indices.len() > self.index_capacity;
|
let index_grew = indices.len() > self.index_capacity;
|
||||||
|
|
||||||
// SAFETY: The renderer owns the queue and all submitted work. Waiting
|
if !vertex_upload.is_empty() || !index_upload.is_empty() {
|
||||||
// before either writing or replacing a shared allocation keeps old
|
self.wait_for_submitted_frames()?;
|
||||||
// command buffers from observing a partial upload.
|
}
|
||||||
unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
|
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
|
||||||
context: "vkDeviceWaitIdle(dynamic mesh update)",
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let (new_vertex_buffer, new_index_buffer) = if vertex_grew || index_grew {
|
let (new_vertex_buffer, new_index_buffer) = if vertex_grew || index_grew {
|
||||||
let instance = self.instance_ref()?;
|
let instance = self.instance_ref()?;
|
||||||
@@ -712,9 +714,12 @@ impl VulkanSmokeRenderer {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
if !vertex_grew {
|
if !vertex_grew {
|
||||||
if let Err(error) =
|
if let Err(error) = write_static_mesh_vertex_buffer(
|
||||||
write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)
|
device,
|
||||||
{
|
self.vertex_buffer_ref()?,
|
||||||
|
vertex_upload.start,
|
||||||
|
&vertices[vertex_upload.clone()],
|
||||||
|
) {
|
||||||
if let Some(buffer) = new_vertex_buffer.as_ref() {
|
if let Some(buffer) = new_vertex_buffer.as_ref() {
|
||||||
destroy_allocated_buffer(device, buffer);
|
destroy_allocated_buffer(device, buffer);
|
||||||
}
|
}
|
||||||
@@ -725,9 +730,12 @@ impl VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !index_grew {
|
if !index_grew {
|
||||||
if let Err(error) =
|
if let Err(error) = write_static_mesh_index_buffer(
|
||||||
write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)
|
device,
|
||||||
{
|
self.index_buffer_ref()?,
|
||||||
|
index_upload.start,
|
||||||
|
&indices[index_upload.clone()],
|
||||||
|
) {
|
||||||
if let Some(buffer) = new_vertex_buffer.as_ref() {
|
if let Some(buffer) = new_vertex_buffer.as_ref() {
|
||||||
destroy_allocated_buffer(device, buffer);
|
destroy_allocated_buffer(device, buffer);
|
||||||
}
|
}
|
||||||
@@ -740,8 +748,18 @@ impl VulkanSmokeRenderer {
|
|||||||
(new_vertex_buffer, new_index_buffer)
|
(new_vertex_buffer, new_index_buffer)
|
||||||
} else {
|
} else {
|
||||||
let device = self.device_ref()?;
|
let device = self.device_ref()?;
|
||||||
write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)?;
|
write_static_mesh_vertex_buffer(
|
||||||
write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)?;
|
device,
|
||||||
|
self.vertex_buffer_ref()?,
|
||||||
|
vertex_upload.start,
|
||||||
|
&vertices[vertex_upload.clone()],
|
||||||
|
)?;
|
||||||
|
write_static_mesh_index_buffer(
|
||||||
|
device,
|
||||||
|
self.index_buffer_ref()?,
|
||||||
|
index_upload.start,
|
||||||
|
&indices[index_upload.clone()],
|
||||||
|
)?;
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -767,6 +785,19 @@ impl VulkanSmokeRenderer {
|
|||||||
geometry_ranges,
|
geometry_ranges,
|
||||||
&mut self.draw_order_dirty,
|
&mut self.draw_order_dirty,
|
||||||
);
|
);
|
||||||
|
self.report.mesh_vertex_upload_bytes = self
|
||||||
|
.report
|
||||||
|
.mesh_vertex_upload_bytes
|
||||||
|
.saturating_add((vertex_upload.len() as u64).saturating_mul(56));
|
||||||
|
self.report.mesh_index_upload_bytes = self
|
||||||
|
.report
|
||||||
|
.mesh_index_upload_bytes
|
||||||
|
.saturating_add((index_upload.len() as u64).saturating_mul(4));
|
||||||
|
self.report.mesh_full_upload_bytes = self
|
||||||
|
.report
|
||||||
|
.mesh_full_upload_bytes
|
||||||
|
.saturating_add((vertices.len() as u64).saturating_mul(56))
|
||||||
|
.saturating_add((indices.len() as u64).saturating_mul(4));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +807,7 @@ impl VulkanSmokeRenderer {
|
|||||||
///
|
///
|
||||||
/// The updated image must have the same dimensions and generated/authored
|
/// The updated image must have the same dimensions and generated/authored
|
||||||
/// mip count as the texture allocated during renderer creation. The
|
/// mip count as the texture allocated during renderer creation. The
|
||||||
/// renderer idles its device before the transfer, so callers may invoke
|
/// renderer waits for submitted readers before the transfer, so callers may invoke
|
||||||
/// this between frame updates without taking ownership of Vulkan handles.
|
/// this between frame updates without taking ownership of Vulkan handles.
|
||||||
pub fn update_material_texture(
|
pub fn update_material_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -807,17 +838,7 @@ impl VulkanSmokeRenderer {
|
|||||||
context: "dynamic material texture index is out of bounds",
|
context: "dynamic material texture index is out of bounds",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
self.wait_for_submitted_frames()?;
|
||||||
let device = self.device_ref()?;
|
|
||||||
// SAFETY: every submission made by this renderer uses this device; the
|
|
||||||
// idle wait makes all descriptor image reads quiescent before upload.
|
|
||||||
unsafe { device.device().device_wait_idle() }.map_err(|result| {
|
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
|
||||||
context: "vkDeviceWaitIdle(dynamic texture update)",
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
let shared_texture = self
|
let shared_texture = self
|
||||||
.texture_slot_indices
|
.texture_slot_indices
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1388,6 +1409,24 @@ impl VulkanSmokeRenderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wait_for_submitted_frames(&self) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let fences: Vec<_> = self.frame_sync.iter().map(|sync| sync.fence).collect();
|
||||||
|
if fences.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// SAFETY: Every graphics submission signals one of these owned fences.
|
||||||
|
// Presentation does not read mesh buffers, textures or descriptor sets.
|
||||||
|
unsafe {
|
||||||
|
self.device_ref()?
|
||||||
|
.device()
|
||||||
|
.wait_for_fences(&fences, true, u64::MAX)
|
||||||
|
}
|
||||||
|
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkWaitForFences(shared resource update)",
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Draws and presents one indexed-triangle frame.
|
/// Draws and presents one indexed-triangle frame.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -1433,14 +1472,7 @@ impl VulkanSmokeRenderer {
|
|||||||
if needs_point_light_growth {
|
if needs_point_light_growth {
|
||||||
// Updating every material's binding 6 and replacing the shared
|
// Updating every material's binding 6 and replacing the shared
|
||||||
// SSBO requires all prior command buffers to finish.
|
// SSBO requires all prior command buffers to finish.
|
||||||
// SAFETY: The renderer owns the logical device and no submission
|
self.wait_for_submitted_frames()?;
|
||||||
// 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 =
|
let instance =
|
||||||
self.instance
|
self.instance
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1464,14 +1496,7 @@ impl VulkanSmokeRenderer {
|
|||||||
// 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
|
||||||
// race a vertex shader from the previous frame.
|
// race a vertex shader from the previous frame.
|
||||||
// SAFETY: The renderer owns the logical device and no submission
|
self.wait_for_submitted_frames()?;
|
||||||
// can observe the host write after this idle point.
|
|
||||||
unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
|
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
|
||||||
context: "vkDeviceWaitIdle(material specular update)",
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let resources = self.resources_ref()?;
|
let resources = self.resources_ref()?;
|
||||||
write_material_specular_buffer(
|
write_material_specular_buffer(
|
||||||
self.device_ref()?,
|
self.device_ref()?,
|
||||||
@@ -2231,6 +2256,23 @@ impl Drop for VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn geometry_upload_range(
|
||||||
|
count: usize,
|
||||||
|
unchanged: usize,
|
||||||
|
capacity: usize,
|
||||||
|
) -> Result<std::ops::Range<usize>, VulkanSmokeRendererError> {
|
||||||
|
if unchanged > count || unchanged > capacity {
|
||||||
|
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "unchanged mesh prefix exceeds uploaded or retained geometry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(if count > capacity {
|
||||||
|
0..count
|
||||||
|
} else {
|
||||||
|
unchanged..count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_geometry_upload(
|
fn validate_geometry_upload(
|
||||||
vertices: &[super::VulkanStaticVertex],
|
vertices: &[super::VulkanStaticVertex],
|
||||||
indices: &[u32],
|
indices: &[u32],
|
||||||
@@ -2572,7 +2614,7 @@ mod tests {
|
|||||||
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
|
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
|
||||||
use super::{
|
use super::{
|
||||||
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
|
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
|
||||||
draw_material_alphas, mark_draw_order_dirty_on_activity_change,
|
draw_material_alphas, geometry_upload_range, mark_draw_order_dirty_on_activity_change,
|
||||||
set_and_mark_dirty_if_changed, sorted_world_draw_order,
|
set_and_mark_dirty_if_changed, sorted_world_draw_order,
|
||||||
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
|
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
|
||||||
validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
|
validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
|
||||||
@@ -2732,6 +2774,15 @@ mod tests {
|
|||||||
assert_eq!(ranges[1].index_count, 3);
|
assert_eq!(ranges[1].index_count, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geometry_upload_preserves_prefix_until_allocation_grows() {
|
||||||
|
assert_eq!(geometry_upload_range(12, 8, 16), Ok(8..12));
|
||||||
|
assert_eq!(geometry_upload_range(8, 8, 16), Ok(8..8));
|
||||||
|
assert_eq!(geometry_upload_range(20, 8, 16), Ok(0..20));
|
||||||
|
assert!(geometry_upload_range(8, 9, 16).is_err());
|
||||||
|
assert!(geometry_upload_range(20, 17, 16).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn geometry_update_can_disable_empty_shadow_range_without_zero_sized_upload() {
|
fn geometry_update_can_disable_empty_shadow_range_without_zero_sized_upload() {
|
||||||
let mesh = VulkanStaticMesh::smoke_triangle();
|
let mesh = VulkanStaticMesh::smoke_triangle();
|
||||||
|
|||||||
@@ -1333,6 +1333,12 @@ pub struct VulkanSmokeRendererReport {
|
|||||||
pub readback_byte_count: u64,
|
pub readback_byte_count: u64,
|
||||||
/// Stable FNV-1a hash of the final completed readback artifact.
|
/// Stable FNV-1a hash of the final completed readback artifact.
|
||||||
pub readback_fnv1a64: u64,
|
pub readback_fnv1a64: u64,
|
||||||
|
/// Vertex bytes written by dynamic geometry uploads, excluding initialization.
|
||||||
|
pub mesh_vertex_upload_bytes: u64,
|
||||||
|
/// Index bytes written by dynamic geometry uploads, excluding initialization.
|
||||||
|
pub mesh_index_upload_bytes: u64,
|
||||||
|
/// Bytes the same updates would write without retaining unchanged prefixes.
|
||||||
|
pub mesh_full_upload_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Measured validation counters from the live smoke loop.
|
/// Measured validation counters from the live smoke loop.
|
||||||
|
|||||||
@@ -4638,6 +4638,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] {
|
|||||||
|
|
||||||
struct StaticPreviewScene {
|
struct StaticPreviewScene {
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
|
dynamic_vertex_start: usize,
|
||||||
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
||||||
native_handedness_by_range: HashMap<usize, i8>,
|
native_handedness_by_range: HashMap<usize, i8>,
|
||||||
type16_present_ranges: HashSet<usize>,
|
type16_present_ranges: HashSet<usize>,
|
||||||
@@ -8052,6 +8053,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
&terrain_materials,
|
&terrain_materials,
|
||||||
Some(&mut material_animations),
|
Some(&mut material_animations),
|
||||||
)?;
|
)?;
|
||||||
|
let dynamic_vertex_start = mesh.vertices.len();
|
||||||
let mut mesh_components = 0;
|
let mut mesh_components = 0;
|
||||||
for (object_index, mission_root) in roots.iter().enumerate() {
|
for (object_index, mission_root) in roots.iter().enumerate() {
|
||||||
let visual_ids = assets.visuals_for_object(object_index);
|
let visual_ids = assets.visuals_for_object(object_index);
|
||||||
@@ -8539,6 +8541,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
let shadow_material_index = append_shadow_material(&mut materials)?;
|
let shadow_material_index = append_shadow_material(&mut materials)?;
|
||||||
Ok(StaticPreviewScene {
|
Ok(StaticPreviewScene {
|
||||||
mesh,
|
mesh,
|
||||||
|
dynamic_vertex_start,
|
||||||
terrain_lod,
|
terrain_lod,
|
||||||
native_handedness_by_range,
|
native_handedness_by_range,
|
||||||
type16_present_ranges,
|
type16_present_ranges,
|
||||||
@@ -9352,6 +9355,7 @@ fn run_static_vulkan_mode(
|
|||||||
|
|
||||||
struct StaticVulkanApp {
|
struct StaticVulkanApp {
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
|
dynamic_vertex_start: usize,
|
||||||
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
terrain_lod: Option<terrain_lod::TerrainLodDraws>,
|
||||||
native_handedness_by_range: HashMap<usize, i8>,
|
native_handedness_by_range: HashMap<usize, i8>,
|
||||||
type16_present_ranges: HashSet<usize>,
|
type16_present_ranges: HashSet<usize>,
|
||||||
@@ -9459,6 +9463,7 @@ impl StaticVulkanApp {
|
|||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
mesh: preview.mesh,
|
mesh: preview.mesh,
|
||||||
|
dynamic_vertex_start: preview.dynamic_vertex_start,
|
||||||
terrain_lod: preview.terrain_lod,
|
terrain_lod: preview.terrain_lod,
|
||||||
native_handedness_by_range: preview.native_handedness_by_range,
|
native_handedness_by_range: preview.native_handedness_by_range,
|
||||||
type16_present_ranges: preview.type16_present_ranges,
|
type16_present_ranges: preview.type16_present_ranges,
|
||||||
@@ -9849,6 +9854,7 @@ impl StaticVulkanApp {
|
|||||||
|
|
||||||
fn update_shadow(
|
fn update_shadow(
|
||||||
mesh: &mut VulkanStaticMesh,
|
mesh: &mut VulkanStaticMesh,
|
||||||
|
dynamic_vertex_start: usize,
|
||||||
shadow_cache: &mut ShadowPageCache,
|
shadow_cache: &mut ShadowPageCache,
|
||||||
shadow_casters: &[ShadowCaster],
|
shadow_casters: &[ShadowCaster],
|
||||||
shadow_receivers: &[ShadowTriangle],
|
shadow_receivers: &[ShadowTriangle],
|
||||||
@@ -9894,7 +9900,13 @@ impl StaticVulkanApp {
|
|||||||
*uploaded_shadow_atlas = atlas;
|
*uploaded_shadow_atlas = atlas;
|
||||||
}
|
}
|
||||||
renderer
|
renderer
|
||||||
.update_mesh_geometry(&mesh.vertices, &mesh.indices, &[geometry])
|
.update_mesh_geometry(
|
||||||
|
&mesh.vertices,
|
||||||
|
&mesh.indices,
|
||||||
|
&[geometry],
|
||||||
|
dynamic_vertex_start,
|
||||||
|
shadow_base_index,
|
||||||
|
)
|
||||||
.map_err(|error| format!("upload environment and shadow geometry: {error}"))?;
|
.map_err(|error| format!("upload environment and shadow geometry: {error}"))?;
|
||||||
Ok(ShadowFrameEvidence {
|
Ok(ShadowFrameEvidence {
|
||||||
visible_casters: visible_casters.len(),
|
visible_casters: visible_casters.len(),
|
||||||
@@ -10444,6 +10456,7 @@ impl StaticVulkanApp {
|
|||||||
.ok_or_else(|| "projected shadow material is out of bounds".to_string())?;
|
.ok_or_else(|| "projected shadow material is out of bounds".to_string())?;
|
||||||
self.shadow_last_evidence = Self::update_shadow(
|
self.shadow_last_evidence = Self::update_shadow(
|
||||||
&mut self.mesh,
|
&mut self.mesh,
|
||||||
|
self.dynamic_vertex_start,
|
||||||
&mut self.shadow_cache,
|
&mut self.shadow_cache,
|
||||||
&self.shadow_casters,
|
&self.shadow_casters,
|
||||||
&self.shadow_receivers,
|
&self.shadow_receivers,
|
||||||
@@ -10468,6 +10481,7 @@ impl StaticVulkanApp {
|
|||||||
.ok_or_else(|| "projected shadow material is out of bounds".to_string())?;
|
.ok_or_else(|| "projected shadow material is out of bounds".to_string())?;
|
||||||
self.shadow_last_evidence = Self::update_shadow(
|
self.shadow_last_evidence = Self::update_shadow(
|
||||||
&mut self.mesh,
|
&mut self.mesh,
|
||||||
|
self.dynamic_vertex_start,
|
||||||
&mut self.shadow_cache,
|
&mut self.shadow_cache,
|
||||||
&self.shadow_casters,
|
&self.shadow_casters,
|
||||||
&self.shadow_receivers,
|
&self.shadow_receivers,
|
||||||
@@ -10597,6 +10611,12 @@ impl StaticVulkanApp {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
eprintln!(
|
||||||
|
"render_geometry_upload vertex_bytes={} index_bytes={} full_upload_bytes={}",
|
||||||
|
report.renderer_report.mesh_vertex_upload_bytes,
|
||||||
|
report.renderer_report.mesh_index_upload_bytes,
|
||||||
|
report.renderer_report.mesh_full_upload_bytes,
|
||||||
|
);
|
||||||
self.output = Some(format!(
|
self.output = Some(format!(
|
||||||
"rendered mission {}: {} frames, {} objects, {} mesh components, {} terrain components, camera={}, captured_viewport_crop={}, control_clock_ms={}, control_clock_mode={}, materials={}, environment_primitives={}, shadow_casters={}, shadow_indices={}, swapchain_recreates={}, validation={}, swapchain_format={}, readback_format={}, readback={}, readback_sequence={}, startup_ms={{mission_assets:{:.1},terrain_materials:{:.1},sky:{:.1},gpu_initialization:{:.1}}}",
|
"rendered mission {}: {} frames, {} objects, {} mesh components, {} terrain components, camera={}, captured_viewport_crop={}, control_clock_ms={}, control_clock_mode={}, materials={}, environment_primitives={}, shadow_casters={}, shadow_indices={}, swapchain_recreates={}, validation={}, swapchain_format={}, readback_format={}, readback={}, readback_sequence={}, startup_ms={{mission_assets:{:.1},terrain_materials:{:.1},sky:{:.1},gpu_initialization:{:.1}}}",
|
||||||
self.mission,
|
self.mission,
|
||||||
@@ -13616,6 +13636,7 @@ mod tests {
|
|||||||
let view_camera = free_camera.preview_frame(16.0 / 9.0);
|
let view_camera = free_camera.preview_frame(16.0 / 9.0);
|
||||||
let preview = StaticPreviewScene {
|
let preview = StaticPreviewScene {
|
||||||
terrain_lod: None,
|
terrain_lod: None,
|
||||||
|
dynamic_vertex_start: 3,
|
||||||
mesh: VulkanStaticMesh {
|
mesh: VulkanStaticMesh {
|
||||||
vertices: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
vertices: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1151,6 +1151,21 @@ WEAR и MAT0 до объединения геометрии. Индексы GPU
|
|||||||
`u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и
|
`u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и
|
||||||
animation phase; object batches сохраняют их legacy blend/depth/alpha state.
|
animation phase; object batches сохраняют их legacy blend/depth/alpha state.
|
||||||
|
|
||||||
|
Рельеф образует неизменяемое начало общего vertex buffer. Control-анимация,
|
||||||
|
FX и окружение обновляют следующие вершины, а новые индексы нужны только
|
||||||
|
геометрии проекционных теней в конце index buffer. Поэтому кадр записывает
|
||||||
|
лишь эти изменяемые части. При росте буфера новая память получает все данные,
|
||||||
|
включая неизменяемое начало; уменьшение числа теней не уменьшает выделенную
|
||||||
|
ёмкость. Это сохраняет номера вершин, диапазоны отрисовки и порядок проходов.
|
||||||
|
|
||||||
|
Перед записью общих ресурсов renderer ждёт fences всех отправленных кадров.
|
||||||
|
Одного fence текущего кадрового слота недостаточно: другой кадр ещё может
|
||||||
|
читать ту же память. Ожидание всего устройства остаётся на пересоздании
|
||||||
|
swapchain, диагностическом readback и завершении работы. Загрузка изменённых
|
||||||
|
текстур пока использует отдельную синхронную transfer-команду. Строка
|
||||||
|
`render_geometry_upload` показывает записанные vertex/index bytes и объём,
|
||||||
|
который потребовался бы при полной перезаливке, без начальной загрузки.
|
||||||
|
|
||||||
После загрузки world path создаёт интерактивную free-flight камеру (`WASD`,
|
После загрузки world path создаёт интерактивную free-flight камеру (`WASD`,
|
||||||
`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом
|
`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом
|
||||||
кадре собирает environment frame. Для каждого отрисовываемого строения
|
кадре собирает environment frame. Для каждого отрисовываемого строения
|
||||||
|
|||||||
Reference in new issue
Block a user