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

  1. Rebuild one mesh with the same PrimitiveTopology you pass to Mesh::new for the other
  2. Convert the strip mesh's triangle list (re-expand strip indices into triangles) before merging
  3. 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

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


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)