You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add native Draco and meshopt codec plugins (#1797)
Adds native C++ implementations of the Draco and meshopt mesh decoders
as two
new optional plugins, `NativeDraco` and `NativeMeshopt`.
Today Babylon Native runs these codecs through the same WASM modules the
web
uses. That pulls a WASM runtime into a native app purely to decompress
meshes,
costs a module instantiation per session, and copies buffers across the
JS/WASM
boundary. These plugins let the native host do the work directly.
### What's here
| Plugin | Exposes | Replaces |
| --- | --- | --- |
| `NativeDraco` | `_native.DracoCodec.Decode(data, attributeIds?)` |
`draco_decoder_gltf.wasm` |
| `NativeDraco` | `_native.DracoCodec.Version` | — |
| `NativeMeshopt` | `_native.MeshoptCodec.Decode(source, count, stride,
mode, filter?)` | `meshopt_decoder.wasm` |
| `NativeMeshopt` | `_native.MeshoptCodec.Version` | — |
Each codec is a single object rather than a set of free functions, so
the
JavaScript side needs one feature probe per codec instead of one per
entry
point, and so the object can publish the codec version compiled into the
binary. Version matters because meshoptimizer records its codec version
in the
first header byte and a decoder refuses streams newer than it
understands; a
bare function name gives a caller no way to find that out except by
failing.
`DracoCodec.Decode` supports both call shapes the Babylon loaders use:
the glTF
path, where the caller passes the `EXT/KHR_draco_mesh_compression` map
of
Babylon vertex-buffer kind to Draco unique id, and the standalone `.drc`
path,
where no map is supplied and the plugin probes the standard named
attributes.
Attribute data is de-interleaved and tightly packed per point, mirroring
emscripten's `GetAttributeDataArrayForAllPoints` that the WASM decoder
relies
on, so the returned buffers are drop-in equivalents.
### Dependencies
Two new `FetchContent` dependencies, both pinned to release tags:
- `google/draco` @ `1.5.7`
- `zeux/meshoptimizer` @ `v0.22`
Both are declared `EXCLUDE_FROM_ALL` and built with tests, install,
executables
and JS glue disabled. draco adds its include directories as `PRIVATE`,
so
`Dependencies/CMakeLists.txt` re-exports them as `PUBLIC` on the
combined
target; the target is named `draco` under MSVC and `draco_static`
elsewhere and
both spellings are handled.
draco is additionally built with `DRACO_GLTF_BITSTREAM=ON`, which
compiles only
mesh compression, normal encoding and the standard edgebreaker. That is
the
same subset `draco_decoder_gltf.wasm` is built with, and that module is
what
Babylon.js's default configuration loads, so this costs no capability
the
JavaScript path has. It drops point cloud compression, the predictive
edgebreaker and pre-glTF backwards compatibility; point cloud streams
return a
Draco error instead of decoding, matching Babylon.js.
### Footprint, and why both plugins default to `OFF`
Both options default to `OFF`. Nothing routes to these entry points
until the
Babylon.js side lands, so until then no consumer should be paying for
them.
Measured on Playground, Win32 x64, RelWithDebInfo, same tree, only the
two
options changed:
| configuration | `Playground.exe` | delta |
| --- | ---: | ---: |
| both `OFF` (default) | 11,100,672 B | baseline |
| `NativeMeshopt` only | 11,132,928 B | +31.5 KB |
| both `ON` | 11,734,528 B | +619 KB (+5.7%) |
meshoptimizer is nearly free; draco is +587 KB.
Draco started at +1.64 MB. Two things brought it down, both of which
track
Babylon.js more closely rather than less:
- **No encoder.** Babylon.js does not bundle one either — `DracoDecoder`
loads
`draco_decoder_gltf.wasm` and `DracoEncoder` fetches a separate
`draco_encoder.wasm` on demand. Linking the encoder here would cost
every
binary ~1.2 MB to serve an authoring path Babylon Native does not
exercise.
- **`DRACO_GLTF_BITSTREAM`**, as described above.
Together that is a 65% reduction in draco's contribution, which is close
enough
to what a minidraco fork would buy that staying on the upstream
bitstream looks
like the better trade.
### Trust boundary
Worth stating plainly, because it cuts both ways:
- On **V8** and **JavaScriptCore**, this moves decoding of untrusted
mesh data
out of the WASM sandbox and into the host process.
- On **QuickJS** and **Hermes** there is no WebAssembly at all, so this
is
net-new capability rather than a relocation of existing capability.
- It removes a runtime fetch of executable code from a CDN, since no
`.wasm`
ships in this tree.
That is the reason the input-validation and malformed/truncated-input
tests
below exist, and why the codecs validate every caller-supplied extent
rather
than trusting the calling loader.
### Scope
The change is **additive**. Nothing existing is modified except the
CMake
wiring that registers the new plugins, four lines in
`Embedding/Source/Runtime.cpp` that initialize them, the `UnitTests`
wiring for
the new coverage, and the three CI workflows that enable the plugins for
the
jobs which run `UnitTests`. No existing plugin, engine path, or test
configuration is touched, and `Apps/Playground/Scripts/config.json` is
deliberately left alone.
### On validation
These entry points are not reachable from the currently pinned
`babylonjs`
npm package (9.15.0), so the plugins are inert until the corresponding
Babylon.js side lands and the loaders start feature-detecting them. The
validation suite therefore cannot exercise this code, but
`Apps/UnitTests`
can, and now does.
Both plugins are linked and initialized in `Apps/UnitTests` behind their
options, and `tests.javaScript.all.ts` carries a describe block for each
— 16
cases covering decoding and malformed, truncated and out-of-range input
for
every entry point. The blocks report as **skipped** when the build did
not opt
in, rather than passing vacuously; CI enables both options for the
Linux, macOS
and Win32 jobs that run `UnitTests`, including the sanitizer
configurations.
Neither codec's positive test is self-referential. Since neither plugin
exposes
an encoder, both fixtures come from the reference JavaScript encoders,
which
pins these decoders to the upstream bitstreams rather than to
themselves:
- **Draco** — two fixtures from `draco3dgltf` **1.5.7**, the same
package
Babylon.js takes its decoder from. One is a two-triangle mesh; the other
is a
63-vertex sphere carrying POSITION, NORMAL and TEX_COORD, which covers
multi-attribute decoding and a non-degenerate edgebreaker traversal —
the
parts `DRACO_GLTF_BITSTREAM` actually restricts.
- **meshopt** — a stream from the reference meshoptimizer **0.22.0**
encoder,
with the test asserting the native decoder reproduces the original bytes
exactly.
The sphere fixture is worth a note. It decodes to **62** vertices and 91
triangles, not the 63 and 96 that were encoded: Draco merges points
whose
attributes all match and drops the degenerate triangles at the poles.
Rather
than adjust the expected numbers to whatever this decoder produced, they
were
taken from the reference `draco3dgltf` decoder run over the same buffer,
which
reports 62 points and 91 faces. This decoder reproduces that exactly.
meshoptimizer's vertex codec is versioned in the first header byte.
Encoders
from 0.23.0 onward emit format version 1 (`0xa1`), which a v0.22 decoder
correctly rejects with `-1`, so the encoder used to produce the fixture
was
version-matched deliberately. That version is now also pinned at compile
time
with a `static_assert`, because bgfx vendors its own copy of
meshoptimizer and
the dependency guard skips our `FetchContent` when a target of that name
already exists — a mismatch there would surface as silent data
corruption
rather than a build break.
- **No regression.** Full validation suite on this branch:
`ran=295 passed=295 failed=0 missingRef=0 skipped=420`, identical to the
upstream `master` baseline. (Indices 53–57 are excluded from the run:
they
crash in `RendererContextD3D11::submit` on pristine `master` too.)
- **Unit tests.** `JavaScript.All` — 42 passing, 0 failing.
### Note for the Babylon.js side
`dracoEncoder.ts` on the Babylon.js side feature-detects
`_native.encodeDracoMesh`.
With no native encoder that probe is simply false and it falls back
exactly as
it does today, so nothing breaks — but the paired Babylon.js change
should drop
that hunk rather than ship a probe that can never succeed. If the
capability is
wanted, the encoder can come back behind an off-by-default option.
---------
Co-authored-by: bkaradzic-microsoft <bkaradzic@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
0 commit comments