bevyengine/bevy · critical

Mismatched vertex attribute values

Error message

Mismatched vertex attribute values

What it means

Defensive panic inside VertexAttributeValues::push_from, the vertex-copy helper used by Mesh::merge_duplicate_vertices (crates/bevy_mesh/src/mesh.rs:1465). It copies one vertex's attribute value from a source attribute store into a destination store, and this arm fires when the destination attribute is VertexAttributeValues::Float32 but the source value is a different variant. Within bevy both sides come from the same attribute map, so via public API this indicates corrupted mesh state or an engine bug; the user-facing cousin for two-mesh merges is MeshMergeError::IncompatibleVertexAttributes.

Source

Thrown at crates/bevy_mesh/src/vertex.rs:706

            VertexAttributeValues::Float64(values) => bytes_of(&values[i]),
            VertexAttributeValues::Float64x2(values) => bytes_of(&values[i]),
            VertexAttributeValues::Float64x3(values) => bytes_of(&values[i]),
            VertexAttributeValues::Float64x4(values) => bytes_of(&values[i]),
            VertexAttributeValues::Unorm10_10_10_2(values) => bytes_of(&values[i]),
            VertexAttributeValues::Unorm8x4Bgra(values) => bytes_of(&values[i]),
        }
    }

    #[expect(
        clippy::match_same_arms,
        reason = "Although the `values` binding on some match arms may have matching types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever."
    )]
    pub(crate) fn push_from(&mut self, source: &VertexAttributeValues, i: usize) {
        match (self, source) {
            (VertexAttributeValues::Float32(this), VertexAttributeValues::Float32(source)) => {
                this.push(source[i]);
            }
            (VertexAttributeValues::Float32(_), _) => panic!("Mismatched vertex attribute values"),
            (VertexAttributeValues::Sint32(this), VertexAttributeValues::Sint32(source)) => {
                this.push(source[i]);
            }
            (VertexAttributeValues::Sint32(_), _) => panic!("Mismatched vertex attribute values"),
            (VertexAttributeValues::Uint32(this), VertexAttributeValues::Uint32(source)) => {
                this.push(source[i]);
            }
            (VertexAttributeValues::Uint32(_), _) => panic!("Mismatched vertex attribute values"),
            (VertexAttributeValues::Float32x2(this), VertexAttributeValues::Float32x2(source)) => {
                this.push(source[i]);
            }
            (VertexAttributeValues::Float32x2(_), _) => {
                panic!("Mismatched vertex attribute values")
            }
            (VertexAttributeValues::Sint32x2(this), VertexAttributeValues::Sint32x2(source)) => {
                this.push(source[i]);
            }
            (VertexAttributeValues::Sint32x2(_), _) => panic!("Mismatched vertex attribute values"),

View on GitHub (pinned to 396ca72708)

Solutions

  1. Treat this as an engine bug or corrupted mesh state: minimize the mesh that triggers it and report it to the bevy issue tracker with a repro
  2. Audit any custom code that constructs or mutates Mesh attribute maps directly and ensure each attribute id always maps to one VertexAttributeValues variant
  3. If you were merging two meshes with different formats (e.g. Float32 vs Float32x3 for the same attribute name), normalize both to the same format first and use Mesh::merge, which reports incompatibilities as a Result instead of panicking
Defensive patterns

Strategy: validation

Validate before calling

// Defensive check before deduplicating or merging: one variant per attribute id
fn attribute_variants_consistent(mesh: &Mesh) -> bool {
    // Well-formed meshes always satisfy this; useful after custom manipulation
    mesh.attributes().all(|(_, data)| {
        VertexFormat::from(&data.values) == VertexFormat::from(&data.values)
    })
}

// Practical guard for merging two meshes (the user-facing cousin)
fn mergeable(a: &Mesh, b: &Mesh) -> bool {
    a.primitive_topology() == b.primitive_topology()
        && a.attributes().all(|(id, val)| {
            b.attribute_by_id(*id)
                .map_or(true, |other| std::mem::discriminant(val) == std::mem::discriminant(other))
        })
}

Type guard

fn attribute_formats_match(a: &VertexAttributeValues, b: &VertexAttributeValues) -> bool {
    std::mem::discriminant(a) == std::mem::discriminant(b)
}

Prevention

When it happens

Trigger: Mesh::merge_duplicate_vertices on a mesh whose attribute state is internally inconsistent, such that the same attribute id resolves to a Float32 store on one side and a non-Float32 store on the other. Not reachable by merging two ordinary meshes (that path returns a Result error instead).

Common situations: Almost always an engine-level invariant violation: a bug in custom mesh-manipulation code or a corrupted/deserialized Mesh. Practically unreachable for well-formed meshes, since push_from is pub(crate) and both operands share one attribute map.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/80fd761534ed5904. Report an issue: GitHub.