Skip to content

meshletcodec: Implement NEON decoding support - #1009

Merged
zeux merged 5 commits into
masterfrom
mlc-neon
Jan 28, 2026
Merged

zeux merged 5 commits into
masterfrom
mlc-neon

Conversation

@zeux

@zeux zeux commented Jan 27, 2026

Copy link
Copy Markdown
Owner

The NEON code mostly follows SSE implementation, with a few adjustments,
such as the use of vmovn for 16-bit truncation, and 16->8 byte shuffles when
possible.

Instead of duplicating the existing functions with their flow, we only
duplicate the building blocks (decodeVertexGroup & decodeTriangleGroup),
and patch the other functions using ifdefs for the short segments of SIMD
code. This appears to be reasonably easy to follow still, and hopefully
will make it easier to maintain this code in the future.

Triangle decoding relies on vqtbl1q which is an AArch64 instruction; while
we could synthesize it out of vqtbl1 on 32-bit ARM, meshlet decoding is a
fairly cutting edge feature that should not need 32-bit support, so we use
a scalar fallback there for simplicity.

The resulting code runs at ~1.7-2B triangles/second when decoding meshlets
with vertex references, and ~2.5-3B triangles/second when decoding triangle-only
data on Apple M4. The "raw" decoding is curiously a little slower because it needs
an extra shuffle (compared to vector shift) to repack the state bytes, so the optimal
path would decode 3-byte triangles.

This contribution is sponsored by Valve.

zeux added 5 commits January 27, 2026 10:17
This is a line-by-line translation of SSE implementation; as such it's not
always optimal and there might be better instruction or intrinsic choices
available in some places.

Instead of duplicating the existing functions with their flow, we only
duplicate the building blocks (decodeVertexGroup & decodeTriangleGroup),
and patch the other functions using ifdefs for the short segments of SIMD
code. This appears to be reasonably easy to follow still, and hopefully
will make it easier to maintain this code in the future.

Triangle decoding relies on vqtbl1q which is an AArch64 instruction; while
we could synthesize it out of vqtbl1 on 32-bit ARM, meshlet decoding is a
fairly cutting edge feature that should not need 32-bit support, so we use
a scalar fallback there for simplicity.
Unlike SSE, NEON has a native instruction to narrow 32-bit integers with
truncation, which is exactly what we need here.
Instead of relying on the compiler to synthesize lane store, use the
intrinsic. This also fixes int->unsigned int casts from unaligned_int.

As part of this, unaligned_int type becomes unused in NEON; move it to
shared code to avoid duplication and only define it for SSE.
We now try to store the values using the "expected" type for the value;
this is mostly for aesthetics as the types here are erased.
This is the more important path for non-raw meshlet decoding so we
should test it instead of 4-byte writes. Note that the benchmark code
doesn't test "raw" decoding; raw decoding is mostly useful when the
output vertex/triangle data needs to be post-processed via SIMD, so it
should not be benchmarked in isolation.
@zeux
zeux merged commit ff565a6 into master Jan 28, 2026
13 checks passed
@zeux
zeux deleted the mlc-neon branch January 28, 2026 18:07
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.

1 participant