bevyengine/bevy · error · MeshMergeError
Incompatible primitive topologies
Error message
Incompatible primitive topologies: {:?} and {:?} What it means
MeshMergeError::IncompatiblePrimitiveTopology (crates/bevy_mesh/src/mesh.rs:3030), returned by Mesh::merge as its very first check (mesh.rs:2135). Merging concatenates index buffers with an offset, which is only meaningful when both meshes interpret those indices under the same primitive topology; mixing e.g. TriangleList with TriangleStrip would produce garbage geometry, so it is rejected up front.
Solutions
- Rebuild one mesh with the same PrimitiveTopology you pass to Mesh::new for the other
- Convert the strip mesh's triangle list (re-expand strip indices into triangles) before merging
- Batch by topology: group meshes into merge sets that share a topology instead of merging across groups
Example fix
// before
let mut a = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
let b = Mesh::new(PrimitiveTopology::TriangleStrip, RenderAssetUsages::default());
a.merge(&b); // Err(IncompatiblePrimitiveTopology { self: TriangleList, other: TriangleStrip })
// after: build both with the same topology
let b = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
a.merge(&b).unwrap(); Defensive patterns
Strategy: validation
Validate before calling
fn same_topology(a: &Mesh, b: &Mesh) -> bool {
a.primitive_topology() == b.primitive_topology()
}
if same_topology(&mesh_a, &mesh_b) {
mesh_a.merge(&mesh_b)?;
} Type guard
fn mergeable_pair<'a>(a: &'a mut Mesh, b: &'a Mesh) -> Option<&'a mut Mesh> {
(a.primitive_topology() == b.primitive_topology()).then_some(a)
} Try / catch
if let Err(MeshMergeError::IncompatiblePrimitiveTopology {
self_primitive_topology,
other_primitive_topology,
}) = mesh_a.merge(&mesh_b)
{
bevy::log::error!("topology mismatch: {self_primitive_topology:?} vs {other_primitive_topology:?}");
} Prevention
- Set topology once in a shared factory for all meshes that will be batched together
- Group merge candidates by PrimitiveTopology before merging
- Beware TriangleStrip leftovers when porting older mesh code
When it happens
Trigger: Calling mesh_a.merge(&mesh_b) where mesh_a was created with PrimitiveTopology::TriangleList and mesh_b with PrimitiveTopology::TriangleStrip (or any other differing variant such as LineList vs LineStrip).
Common situations: Merging a Bevy primitive (always TriangleList) with a custom mesh built as TriangleStrip to save indices; mixing 2D line geometry (LineList) with triangle meshes in a batching pass; topology defaults changing between asset importer versions.
Related errors
- cannot generate tangents for
- Meshes used with Extrusions must have a primitive topology…
- Incompatible vertex attribute types
- Mismatched vertex attribute values
- unsupported primitive mode
AI-assisted analysis of bevyengine/bevy@8d743eb7dc (2026-08-20).
Data as JSON: /api/errors/56f98019016600d8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/bevy_mesh/src/mesh.rs:3110
/// Use [`MeshDeserializer`] if you need to pass extra options to the deserialization process, such as specifying custom vertex attributes.
pub fn into_mesh(self) -> Mesh {
MeshDeserializer::default().deserialize(self)
}
}
/// Use to specify extra options when deserializing a [`SerializedMesh`] into a [`Mesh`].
#[cfg(feature = "serialize")]
pub struct MeshDeserializer {
custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
}
#[cfg(feature = "serialize")]
impl Default for MeshDeserializer {
fn default() -> Self {
// Written like this so that the compiler can validate that we use all the built-in attributes.
// If you just added a new attribute and got a compile error, please add it to this list :)
const BUILTINS: [MeshVertexAttribute; Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE as usize] = [
Mesh::ATTRIBUTE_POSITION,
Mesh::ATTRIBUTE_NORMAL,
Mesh::ATTRIBUTE_UV_0,
Mesh::ATTRIBUTE_UV_1,
Mesh::ATTRIBUTE_TANGENT,
Mesh::ATTRIBUTE_COLOR,
Mesh::ATTRIBUTE_JOINT_WEIGHT,
Mesh::ATTRIBUTE_JOINT_INDEX,
];
Self {
custom_vertex_attributes: BUILTINS
.into_iter()
.map(|attribute| (attribute.name.into(), attribute))
.collect(),
}
}
}
#[cfg(feature = "serialize")]View on GitHub (pinned to 8d743eb7dc)