aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-11-01 00:09:36 +0300
committerValentin Popov <valentin@popov.link>2026-11-01 00:09:36 +0300
commit5890416fd74f4868d16814bd87221370e34062db (patch)
treee0cbc447d76907e423454b371ae735d1a4b70b47 /adapters/fparkan-render-vulkan/src/ffi
parent8fab4b44b8241ffc6450780c6590908f4406f4da (diff)
downloadfparkan-5890416fd74f4868d16814bd87221370e34062db.tar.xz
fparkan-5890416fd74f4868d16814bd87221370e34062db.zip
perf(render): retain static mesh prefixes between frames
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/resources.rs41
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs163
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs6
3 files changed, 140 insertions, 70 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
index 0b43565..fe71d12 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs
@@ -243,18 +243,25 @@ pub(super) fn create_vertex_buffer(
/// 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
/// geometry between frames.
pub(super) fn write_static_mesh_vertex_buffer(
device: &VulkanLogicalDeviceProbe,
buffer: &VulkanAllocatedBuffer,
+ first_vertex: usize,
vertices: &[super::VulkanStaticVertex],
) -> Result<(), VulkanSmokeRendererError> {
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(
device,
buffer,
+ offset,
&bytes,
"vkMapMemory(static mesh vertex update)",
)
@@ -295,29 +302,28 @@ fn encode_static_vertices(
fn write_mapped_buffer(
device: &VulkanLogicalDeviceProbe,
buffer: &VulkanAllocatedBuffer,
+ offset: usize,
bytes: &[u8],
context: &'static str,
) -> Result<(), VulkanSmokeRendererError> {
- let byte_len =
- u64::try_from(bytes.len()).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh {
- context: "static mesh upload exceeds address space",
- })?;
- if byte_len == 0 {
- return Err(VulkanSmokeRendererError::InvalidStaticMesh {
- context: "static mesh upload cannot be empty",
- });
+ if bytes.is_empty() {
+ return Ok(());
}
// SAFETY: The caller validates that the allocation is large enough and
// waits for all queue work that could read it before this write.
let mapped = unsafe {
- device
- .device()
- .map_memory(buffer.memory, 0, byte_len, vk::MemoryMapFlags::empty())
+ device.device().map_memory(
+ buffer.memory,
+ 0,
+ vk::WHOLE_SIZE,
+ vk::MemoryMapFlags::empty(),
+ )
}
.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 {
- 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);
}
Ok(())
@@ -351,12 +357,19 @@ pub(super) fn create_index_buffer(
pub(super) fn write_static_mesh_index_buffer(
device: &VulkanLogicalDeviceProbe,
buffer: &VulkanAllocatedBuffer,
+ first_index: usize,
indices: &[u32],
) -> Result<(), VulkanSmokeRendererError> {
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(
device,
buffer,
+ offset,
&bytes,
"vkMapMemory(static mesh index update)",
)
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index 0e1cebb..69bf23e 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -464,6 +464,9 @@ impl VulkanSmokeRenderer {
readback_copy_count: 0,
readback_byte_count: 0,
readback_fnv1a64: 0,
+ mesh_vertex_upload_bytes: 0,
+ mesh_index_upload_bytes: 0,
+ mesh_full_upload_bytes: 0,
},
};
renderer.rebuild_swapchain_resources(false)?;
@@ -495,6 +498,9 @@ impl VulkanSmokeRenderer {
readback_copy_count: 0,
readback_byte_count: 0,
readback_fnv1a64: 0,
+ mesh_vertex_upload_bytes: 0,
+ mesh_index_upload_bytes: 0,
+ mesh_full_upload_bytes: 0,
};
Ok(renderer)
}
@@ -628,7 +634,7 @@ impl VulkanSmokeRenderer {
///
/// The initial mesh owns enough vertices for the scene's dynamic slots;
/// 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.
pub fn update_vertices(
&mut self,
@@ -639,15 +645,8 @@ impl VulkanSmokeRenderer {
context: "updated static mesh vertex count differs from allocation",
});
}
- // SAFETY: The renderer owns the queue and all submitted work; waiting
- // for idle makes the shared vertex allocation quiescent.
- 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)
+ self.wait_for_submitted_frames()?;
+ write_static_mesh_vertex_buffer(self.device_ref()?, self.vertex_buffer_ref()?, 0, vertices)
}
/// 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
/// remain untouched. The allocations grow to the largest submitted frame
/// 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
/// frame. The mesh itself still needs at least one vertex and one index so
@@ -667,6 +669,8 @@ impl VulkanSmokeRenderer {
vertices: &[super::VulkanStaticVertex],
indices: &[u32],
geometry_ranges: &[super::VulkanDynamicDrawRange],
+ unchanged_vertices: usize,
+ unchanged_indices: usize,
) -> Result<(), VulkanSmokeRendererError> {
validate_geometry_upload(
vertices,
@@ -676,19 +680,17 @@ impl VulkanSmokeRenderer {
self.vertex_count,
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 index_grew = indices.len() > self.index_capacity;
- // SAFETY: The renderer owns the queue and all submitted work. Waiting
- // before either writing or replacing a shared allocation keeps old
- // 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,
- }
- })?;
+ if !vertex_upload.is_empty() || !index_upload.is_empty() {
+ self.wait_for_submitted_frames()?;
+ }
let (new_vertex_buffer, new_index_buffer) = if vertex_grew || index_grew {
let instance = self.instance_ref()?;
@@ -712,9 +714,12 @@ impl VulkanSmokeRenderer {
None
};
if !vertex_grew {
- if let Err(error) =
- write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)
- {
+ if let Err(error) = write_static_mesh_vertex_buffer(
+ device,
+ self.vertex_buffer_ref()?,
+ vertex_upload.start,
+ &vertices[vertex_upload.clone()],
+ ) {
if let Some(buffer) = new_vertex_buffer.as_ref() {
destroy_allocated_buffer(device, buffer);
}
@@ -725,9 +730,12 @@ impl VulkanSmokeRenderer {
}
}
if !index_grew {
- if let Err(error) =
- write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)
- {
+ if let Err(error) = write_static_mesh_index_buffer(
+ device,
+ self.index_buffer_ref()?,
+ index_upload.start,
+ &indices[index_upload.clone()],
+ ) {
if let Some(buffer) = new_vertex_buffer.as_ref() {
destroy_allocated_buffer(device, buffer);
}
@@ -740,8 +748,18 @@ impl VulkanSmokeRenderer {
(new_vertex_buffer, new_index_buffer)
} else {
let device = self.device_ref()?;
- write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)?;
- write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)?;
+ write_static_mesh_vertex_buffer(
+ 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)
};
@@ -767,6 +785,19 @@ impl VulkanSmokeRenderer {
geometry_ranges,
&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(())
}
@@ -776,7 +807,7 @@ impl VulkanSmokeRenderer {
///
/// The updated image must have the same dimensions and generated/authored
/// 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.
pub fn update_material_texture(
&mut self,
@@ -807,17 +838,7 @@ impl VulkanSmokeRenderer {
context: "dynamic material texture index is out of bounds",
});
}
- {
- 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,
- }
- })?;
- }
+ self.wait_for_submitted_frames()?;
let shared_texture = self
.texture_slot_indices
.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.
///
/// # Errors
@@ -1433,14 +1472,7 @@ impl VulkanSmokeRenderer {
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,
- }
- })?;
+ self.wait_for_submitted_frames()?;
let instance =
self.instance
.as_ref()
@@ -1464,14 +1496,7 @@ impl VulkanSmokeRenderer {
// The storage buffer is shared by all in-flight frames. Wait for
// every prior use before replacing the records so a setter cannot
// race a vertex shader from the previous frame.
- // SAFETY: The renderer owns the logical device and no submission
- // 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,
- }
- })?;
+ self.wait_for_submitted_frames()?;
let resources = self.resources_ref()?;
write_material_specular_buffer(
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(
vertices: &[super::VulkanStaticVertex],
indices: &[u32],
@@ -2572,7 +2614,7 @@ mod tests {
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
use super::{
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,
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
@@ -2733,6 +2775,15 @@ mod tests {
}
#[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]
fn geometry_update_can_disable_empty_shadow_range_without_zero_sized_upload() {
let mesh = VulkanStaticMesh::smoke_triangle();
let geometry = [VulkanDynamicDrawRange {
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index 0996fa1..39b415a 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -1333,6 +1333,12 @@ pub struct VulkanSmokeRendererReport {
pub readback_byte_count: u64,
/// Stable FNV-1a hash of the final completed readback artifact.
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.