Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 172 additions & 76 deletions crates/renderer/src/buffer/dynamic_storage.rs

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion crates/renderer/src/buffer/dynamic_uniform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ pub struct DynamicUniformBuffer<K: Key, const ZERO_VALUE: u8 = 0> {
capacity_slots: usize,
// first unused index >= capacity used so far
next_slot: usize,
#[allow(dead_code)]
label: Option<String>,
byte_size: usize,
aligned_slice_size: usize,
/// The original `initial_capacity` argument passed to [`Self::new`],
/// kept so that [`Self::clear`] can reset to the same starting size.
initial_capacity_arg: usize,
}

impl<K: Key, const ZERO_VALUE: u8> DynamicUniformBuffer<K, ZERO_VALUE> {
Expand Down Expand Up @@ -82,9 +84,24 @@ impl<K: Key, const ZERO_VALUE: u8> DynamicUniformBuffer<K, ZERO_VALUE> {
label,
byte_size,
aligned_slice_size,
initial_capacity_arg: initial_capacity,
}
}

/// Resets the buffer to its initial capacity, dropping all allocations.
///
/// After this call the buffer is in the same state as a freshly
/// constructed one (with the same constructor arguments).
pub fn clear(&mut self) {
let fresh = Self::new(
self.initial_capacity_arg,
self.byte_size,
Some(self.aligned_slice_size),
self.label.clone(),
);
*self = fresh;
}

/// Returns the total byte size of the buffer.
pub fn size(&self) -> usize {
self.raw_data.len()
Expand Down
3 changes: 3 additions & 0 deletions crates/renderer/src/gltf/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ pub enum AwsmGltfError {
#[error("[gltf] unable to generate tangents: {0}")]
GenerateTangents(String),

#[error("[gltf] geometry data size overflow: {0}")]
GeometryDataSizeOverflow(String),

#[error("[gltf] unable to get positions: {0}")]
Positions(String),

Expand Down
8 changes: 7 additions & 1 deletion crates/renderer/src/gltf/populate/extensions/instancing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,13 @@ impl AwsmRenderer {
gltf_node.index()
)))?;

self.instances.transform_insert(key, &transforms);
self.instances
.transform_insert(key, &transforms)
.map_err(|e| {
AwsmGltfError::ExtInstancing(anyhow::anyhow!(
"instance transform buffer overflow: {e}"
))
})?;
ctx.transform_is_instanced.lock().unwrap().insert(key);
}
}
Expand Down
89 changes: 62 additions & 27 deletions crates/renderer/src/gltf/populate/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,33 +212,68 @@ impl AwsmRenderer {
let aabb = try_position_aabb(&gltf_primitive);

let mesh_key = {
let visibility_geometry_data =
match primitive_buffer_info.visibility_geometry_vertex.clone() {
Some(info) => {
let geometry_data_start = info.offset;
let geometry_data_end = geometry_data_start
+ MeshBufferVertexInfo::from(info).visibility_geometry_size();
Some(
&ctx.data.buffers.visibility_geometry_vertex_bytes
[geometry_data_start..geometry_data_end],
)
}
None => None,
};

let transparency_geometry_data =
match primitive_buffer_info.transparency_geometry_vertex.clone() {
Some(info) => {
let geometry_data_start = info.offset;
let geometry_data_end = geometry_data_start
+ MeshBufferVertexInfo::from(info).transparency_geometry_size();
Some(
&ctx.data.buffers.transparency_geometry_vertex_bytes
[geometry_data_start..geometry_data_end],
)
}
None => None,
};
let visibility_geometry_data = match primitive_buffer_info
.visibility_geometry_vertex
.clone()
{
Some(info) => {
let geometry_data_start = info.offset;
let vertex_info = MeshBufferVertexInfo::from(info);
let geometry_size =
vertex_info.checked_visibility_geometry_size().ok_or_else(|| {
AwsmGltfError::GeometryDataSizeOverflow(format!(
"visibility geometry: {} vertices * {} bytes/vertex overflows usize",
vertex_info.count,
MeshBufferVertexInfo::VISIBILITY_GEOMETRY_BYTE_SIZE
))
})?;
let geometry_data_end = geometry_data_start
.checked_add(geometry_size)
.ok_or_else(|| {
AwsmGltfError::GeometryDataSizeOverflow(format!(
"visibility geometry: offset {} + size {} overflows usize",
geometry_data_start, geometry_size
))
})?;
Some(
&ctx.data.buffers.visibility_geometry_vertex_bytes
[geometry_data_start..geometry_data_end],
)
}
None => None,
};

let transparency_geometry_data = match primitive_buffer_info
.transparency_geometry_vertex
.clone()
{
Some(info) => {
let geometry_data_start = info.offset;
let vertex_info = MeshBufferVertexInfo::from(info);
let geometry_size = vertex_info
.checked_transparency_geometry_size()
.ok_or_else(|| {
AwsmGltfError::GeometryDataSizeOverflow(format!(
"transparency geometry: {} vertices * {} bytes/vertex overflows usize",
vertex_info.count,
MeshBufferVertexInfo::TRANSPARENCY_GEOMETRY_BYTE_SIZE
))
})?;
let geometry_data_end = geometry_data_start
.checked_add(geometry_size)
.ok_or_else(|| {
AwsmGltfError::GeometryDataSizeOverflow(format!(
"transparency geometry: offset {} + size {} overflows usize",
geometry_data_start, geometry_size
))
})?;
Some(
&ctx.data.buffers.transparency_geometry_vertex_bytes
[geometry_data_start..geometry_data_end],
)
}
None => None,
};

let custom_attribute_data_start =
primitive_buffer_info.triangles.vertex_attributes_offset;
Expand Down
22 changes: 18 additions & 4 deletions crates/renderer/src/instances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ impl Instances {
}

/// Inserts instance transforms for a key.
pub fn transform_insert(&mut self, key: TransformKey, transforms: &[Transform]) {
pub fn transform_insert(&mut self, key: TransformKey, transforms: &[Transform]) -> Result<()> {
self.cpu_transforms.insert(key, transforms.to_vec());
let bytes = Self::transforms_to_bytes(transforms);
self.transform_buffer.update(key, &bytes);
self.transform_buffer.update(key, &bytes).map_err(|e| {
AwsmInstanceError::BufferCapacityOverflow(format!("instance transforms: {e}"))
Comment on lines +53 to +57
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transform_insert changed from returning () to Result<()> and the method is pub on a pub type. If Instances is part of the crate’s public API, this is a breaking change for downstream users. Consider either bumping the crate version appropriately, or adding a new fallible method (e.g., try_transform_insert) while keeping the old signature for compatibility.

Copilot uses AI. Check for mistakes.
})?;
self.transform_count.insert(key, transforms.len());
self.transform_gpu_dirty = true;
self.transform_dirty.insert(key);
Ok(())
}
Comment on lines +58 to 63
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transform_insert mutates cpu_transforms before performing the now-fallible buffer update. If the buffer update fails, the method returns Err but leaves cpu_transforms (and potentially other maps) partially updated. Consider updating the GPU buffer first (or rolling back the CPU-side inserts on error) so the operation is all-or-nothing.

Copilot uses AI. Check for mistakes.

/// Updates a single instance transform.
Expand Down Expand Up @@ -121,7 +124,11 @@ impl Instances {
.get(key)
.ok_or(AwsmInstanceError::TransformNotFound(key))?;
let full_bytes = Self::transforms_to_bytes(full_list);
self.transform_buffer.update(key, &full_bytes);
self.transform_buffer
.update(key, &full_bytes)
.map_err(|e| {
AwsmInstanceError::BufferCapacityOverflow(format!("instance transforms: {e}"))
})?;
}
self.transform_count.insert(key, len);
self.transform_gpu_dirty = true;
Expand Down Expand Up @@ -281,7 +288,11 @@ impl Instances {
};

existing_bytes.resize(desired_bytes, 0);
self.transform_buffer.update(key, &existing_bytes);
self.transform_buffer
.update(key, &existing_bytes)
.map_err(|e| {
AwsmInstanceError::BufferCapacityOverflow(format!("instance transforms: {e}"))
})?;
self.transform_gpu_dirty = true;
self.transform_dirty.insert(key);

Expand Down Expand Up @@ -314,4 +325,7 @@ pub enum AwsmInstanceError {

#[error("[instance] transform does not exist {0:?}")]
TransformNotFound(TransformKey),

#[error("[instance] buffer capacity overflow: {0}")]
BufferCapacityOverflow(String),
}
16 changes: 12 additions & 4 deletions crates/renderer/src/materials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,18 @@ impl Materials {
}

match material.uniform_buffer_data(textures) {
Ok(data) => {
self.buffer.update(key, &data);
self.gpu_dirty = true;
}
Ok(data) => match self.buffer.update(key, &data) {
Ok(_) => {
self.gpu_dirty = true;
}
Err(e) => {
tracing::error!(
"Failed to update material buffer for key {:?}: {:?}",
key,
e
);
}
},
Comment on lines 180 to +192
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Materials::update now logs storage-buffer update failures but still leaves the in-memory material mutated. On Err, the GPU buffer contents remain stale and subsequent rendering may use outdated uniform data for that key. Consider making this operation fallible (return Result), or rolling back the material mutation / marking the material as invalid when the buffer update fails.

Copilot uses AI. Check for mistakes.
Err(e) => {
tracing::error!(
"Failed to get uniform buffer data for material key {:?}: {:?}",
Expand Down
35 changes: 28 additions & 7 deletions crates/renderer/src/meshes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ impl AwsmRenderer {
mesh.instanced = true;
}

self.instances.transform_insert(transform_key, transforms);
self.instances.transform_insert(transform_key, transforms)?;

let mesh = self.meshes.get(mesh_key)?;
self.render_passes
Expand Down Expand Up @@ -232,7 +232,7 @@ impl AwsmRenderer {
}

self.instances
.transform_insert(mesh.transform_key, transforms);
.transform_insert(mesh.transform_key, transforms)?;

Ok(())
}
Expand Down Expand Up @@ -517,7 +517,12 @@ impl Meshes {
geometry_index.extend_from_slice(&(i as u32).to_le_bytes());
}
self.visibility_geometry_index_buffers
.update(resource_key, &geometry_index);
.update(resource_key, &geometry_index)
.map_err(|e| {
AwsmMeshError::BufferCapacityOverflow(format!(
"visibility geometry index: {e}"
))
})?;
} else {
return Err(AwsmMeshError::VisibilityGeometryBufferInfoNotFound(
buffer_info_key,
Expand All @@ -527,7 +532,12 @@ impl Meshes {
self.visibility_geometry_index_dirty = true;
let offset = self
.visibility_geometry_data_buffers
.update(resource_key, geometry_data);
.update(resource_key, geometry_data)
.map_err(|e| {
AwsmMeshError::BufferCapacityOverflow(format!(
"visibility geometry data: {e}"
))
})?;
self.visibility_geometry_data_dirty = true;

Some(offset)
Expand All @@ -539,7 +549,12 @@ impl Meshes {
Some(geometry_data) => {
let offset = self
.transparency_geometry_data_buffers
.update(resource_key, geometry_data);
.update(resource_key, geometry_data)
.map_err(|e| {
AwsmMeshError::BufferCapacityOverflow(format!(
"transparency geometry data: {e}"
))
})?;
self.transparency_geometry_data_dirty = true;

Some(offset)
Expand All @@ -549,12 +564,18 @@ impl Meshes {

let custom_attribute_indices_offset = self
.custom_attribute_index_buffers
.update(resource_key, attribute_index);
.update(resource_key, attribute_index)
.map_err(|e| {
AwsmMeshError::BufferCapacityOverflow(format!("custom attribute index: {e}"))
})?;
self.custom_attribute_index_dirty = true;

let custom_attribute_data_offset = self
.custom_attribute_data_buffers
.update(resource_key, attribute_data);
.update(resource_key, attribute_data)
.map_err(|e| {
AwsmMeshError::BufferCapacityOverflow(format!("custom attribute data: {e}"))
})?;
self.custom_attribute_data_dirty = true;

// KEEP THIS AROUND FOR DEBUGGING
Expand Down
23 changes: 21 additions & 2 deletions crates/renderer/src/meshes/buffer_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,33 @@ impl MeshBufferVertexInfo {
/// Byte size for instance transform data.
pub const INSTANCING_BYTE_SIZE: usize = 64;

/// Returns the visibility geometry buffer size in bytes, or `None` on overflow.
pub fn checked_visibility_geometry_size(&self) -> Option<usize> {
self.count.checked_mul(Self::VISIBILITY_GEOMETRY_BYTE_SIZE)
}

/// Returns the visibility geometry buffer size in bytes.
///
/// # Panics
/// Panics if the result would overflow `usize`.
pub fn visibility_geometry_size(&self) -> usize {
self.count * Self::VISIBILITY_GEOMETRY_BYTE_SIZE
self.checked_visibility_geometry_size()
.expect("visibility geometry size overflow")
}

/// Returns the transparency geometry buffer size in bytes, or `None` on overflow.
pub fn checked_transparency_geometry_size(&self) -> Option<usize> {
self.count
.checked_mul(Self::TRANSPARENCY_GEOMETRY_BYTE_SIZE)
}

/// Returns the transparency geometry buffer size in bytes.
///
/// # Panics
/// Panics if the result would overflow `usize`.
pub fn transparency_geometry_size(&self) -> usize {
self.count * Self::TRANSPARENCY_GEOMETRY_BYTE_SIZE
self.checked_transparency_geometry_size()
.expect("transparency geometry size overflow")
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/renderer/src/meshes/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@ pub enum AwsmMeshError {

#[error("[mesh] buffer info not found: {0:?}")]
BufferInfoNotFound(MeshBufferInfoKey),

#[error("[mesh] buffer capacity overflow: {0}")]
BufferCapacityOverflow(String),
}
8 changes: 6 additions & 2 deletions crates/renderer/src/meshes/morphs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,12 @@ impl<Key: slotmap::Key, Info: MorphInfo> MorphData<Key, Info> {

let key = self.infos.insert(morph_buffer_info.clone());

self.weights.update(key, weights);
self.values.update(key, values);
self.weights
.update(key, weights)
.map_err(|e| AwsmMeshError::BufferCapacityOverflow(format!("morph weights: {e}")))?;
self.values
.update(key, values)
.map_err(|e| AwsmMeshError::BufferCapacityOverflow(format!("morph values: {e}")))?;
Comment on lines 161 to +168
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insert_raw inserts the morph Info into self.infos before performing the now-fallible storage buffer updates. If weights.update or values.update fails, the method returns Err but the infos entry remains, leaving an orphaned key. Consider performing the fallible buffer updates first (using a temporary key) or removing the infos entry on error.

Copilot uses AI. Check for mistakes.

self.weights_dirty = true;
self.values_dirty = true;
Expand Down
Loading