Skip to content

The Big Refactor #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

bushrat011899
Copy link
Contributor

@bushrat011899 bushrat011899 commented Jul 16, 2025

Objective

It has been discovered through fuzzing that the current bevy_mikktspace is no longer byte-compatible with the original C implementation. Using git bisect, I determined that the discrepancy was introduced before the transition to safe Rust. This makes diagnosing and resolving the issue almost impossible.

So I started again from scratch.

Using c2rust, cargo-fuzz, test data produced by @atlv24, and the original safe translation by @LaylBongers, I recreated a safe port of mikktspace in safe Rust here.

This PR moves the changes there to bevy_mikktspace.

Solution

  • Added fuzzing using cargo-fuzz with built-in comparison to mikktspace-sys (the original C implementation with Rust bindings).
  • Added test data from atlv24 to better catch known edge cases.
  • Adjusted Geometry API to avoid mutually exclusive trait methods for setting tangent values.
  • Migrated no_std support to directly receive the two missing float methods (sqrt and acos) rather than a higher level Vec3 implementation.
  • Added an std feature to provide a byte-compatible default. This is motivated by a desire to make this a viable replacement for mikktspace-sys for non-Bevy use-cases.
  • Massively refactored the original C implementation to more idiomatic Rust. This includes better utilization of iterator techniques rather than preallocated buffers and indexing.
  • Added documentation explaining how and why certain code is employed, especially where a more idiomatic option isn't used.
  • Added features to provide more "correct" results (fixing original C bugs) at the expense of byte-compatibility.
  • Supersedes add test data #5

Notes

This is a complete replacement of the original code, so it may be easier to just check out and read rather than reviewing online.

Copy link
Contributor Author

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

Just some comments to assist reviewers.


[dependencies]
bitflags = "2.3"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of alignment, there was actually no space savings in the Rust implementation using bitflags over the far simpler "just use 4 bools".

Comment on lines +30 to +42
# The below features will cause the resulting values to differ from the original
# C implementation.

# Corrects a sorting bug in the original C implementation.
# See https://github.com/mmikk/MikkTSpace/issues/5 for details
corrected-edge-sorting = []

# Uses a BTreeMap to weld vertices which is guaranteed to use the smallest vertex
# indices.
# This handles `NaN` values differently to the C implementation, so will produce
# different results for poor geometry.
# For typical geometry, this should produce identical results.
corrected-vertex-welding = []
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These features might be worth prefixing with "unstable" so we can hit 1.0.

@@ -23,31 +21,12 @@ struct Mesh {
vertices: Vec<Vertex>,
}

struct GlamSpace;

impl bevy_mikktspace::VectorSpace for GlamSpace {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know this was something I introduced only a few weeks ago, but having spent all that time neck deep in the unsafe C version, I think I prefer the simplicity of just giving the 2 missing float methods.

Plus, an std feature on by default improves the typical user experience.

fn set_tangent_encoded(&mut self, tangent: [f32; 4], face: usize, vert: usize) {
fn set_tangent(
&mut self,
tangent: Option<bevy_mikktspace::TangentSpace>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change allows both set_tangent methods to be combined, avoiding the somewhat ugly "implement one of these" pattern that was a holdover from the original C implementation.

Also note I've explicitly replaced this with an Option. The original C implementation will silently return a default value under certain circumstances, which I think is something worth highlighting to the user, as they may have a more sensible default. TangentSpace implements Default, so users can get the exact behaviour anyway.

@@ -280,7 +264,7 @@ fn make_cube() -> Mesh {

fn main() {
let mut cube = make_cube();
bevy_mikktspace::generate_tangents(&mut cube);
let _ = bevy_mikktspace::generate_tangents(&mut cube);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't return bool in Rust to indicate success! We have Result!

Comment on lines +210 to +218
generate_tests! {
cube: "../data/cube.obj",
crangeract: "../data/crangeract.obj",
doomcone_smooth: "../data/doomcone_smooth.obj",
doomcone: "../data/doomcone.obj",
obliterated: "../data/obliterated.obj",
rancid_geometry: "../data/rancid_geometry.obj",
tangeract: "../data/tangeract.obj",
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For all of these tests we directly compare to the C implementation to ensure identical results.

Comment on lines +21 to +32
mod face_vertex;
#[cfg_attr(
not(feature = "corrected-edge-sorting"),
path = "mikktspace/quick_sort_legacy.rs"
)]
mod quick_sort;
mod raw_tangent_space;
#[cfg_attr(
not(feature = "corrected-vertex-welding"),
path = "mikktspace/weld_vertices_legacy.rs"
)]
mod weld_vertices;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've split out some of this file into a couple of supporting modules, just to help with clarity and feature gating.

///
/// This is separated from [`generate_tangent_space_and_write`] to highlight this
/// step does not require mutable access to the provided [context](MikkTSpaceInterface).
fn generate_tangent_space<I: Geometry<O>, O: Ops>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Compared to the original C implementation, I've reordered some operations to simplify. For example, the vertex list isn't actually used until much later in the algorithm, but it's generated early and must be amended as the triangle list is modified to keep them in sync. Instead, I generate the list towards the end and skip all the intermediate busy work.

) -> Result<Vec<Option<TangentSpace<O>>>, GenerateTangentSpaceError> {
// This set of operations can be done on-line.
// That is, they do not require an intermediatory buffer.
let mut triangle_info_list = generate_triangle_info_list(context, faces_total);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've moved most of the manipulation of triangle information into this method, as the original implementation iterates over the list many times, when it could instead perform most of the tasks in one go. With Rust iterators that's very trivial.

struct TriangleInfo<O: Ops> {
/// Stores the index of each neighboring triangle to this one, if any.
/// Indices correspond to _edges_ of this triangle, rather than _vertices_.
face_neighbors: [Option<usize>; 3],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

usize increases the size of this type a lot, but it's idiomatic Rust so I'd prefer it over u32 indexing...

Copy link
Contributor

Choose a reason for hiding this comment

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

NonMaxUsize? will this always be 0..3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah so this is a bit of bad documentation on my part. The Option<usize> value is an index into the big list of all triangles. The edge value is bounded to 0..3, but is the index into the face_neighbors array itself, not the value stored in each slot. I've updated the documentation to better explain that.

As for NonMaxUsize, I agree, but I'd rather not. The goal is to get this to 1.0 and never worry about it again, whereas the nonmax crate is still pre-1.0. Technically it shouldn't matter, as it wouldn't be a part of the public API, but it'd still potentially cause duplicated dependencies in the future, etc.

@@ -0,0 +1,33 @@
/// New-type index for a particular vertex on a particular face.
Copy link
Member

Choose a reason for hiding this comment

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

Documenting the winding order here would be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The way mikktspace works should be independent of the winding order:

This is done by performing an internal welding step and subsequently an
order-independent evaluation of tangent space for meshes consisting of
triangles and quads.
This means faces can be received in any order and the same is true for the
order of vertices of each face.
The generated result will not be affected by such reordering.

But I'll leave this comment unresolved just in case someone with better rendering experience would like to weigh in!

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Non-malicious, completely safe, generally very clear. I am very much leaning on tests for correctness.

I have a few small nits, and I would like to clarify the copyright notices before we merge this. I know the standard style is "just dump it at the top of the file", but I always find that very unhelpful and unclear about why the notice exists at all.

@bushrat011899
Copy link
Contributor Author

Added a benchmark which compares bevy_mikktspace against mikktspace-sys on a subdivided plane (something simple and can be generated on the fly to allow huge models if desired). For a 32x32 quad, both implementations complete the benchmark in around 1.9ms (plus or minus 0.1ms). Changing the number of subdivisions drastically changes the runtime, but the relative performance stays largely the same; within percentage points of each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants