Skip to content

EXT_mesh_primitive_restart: Performance optimizations and schema updates#100

Closed
donmccurdy wants to merge 1 commit into
mainfrom
donmccurdy/EXT_mesh_primitive_restart
Closed

EXT_mesh_primitive_restart: Performance optimizations and schema updates#100
donmccurdy wants to merge 1 commit into
mainfrom
donmccurdy/EXT_mesh_primitive_restart

Conversation

@donmccurdy
Copy link
Copy Markdown
Member

@donmccurdy donmccurdy commented Mar 31, 2026

Motivation

In its current form, the EXT_mesh_primitive_restart extension encodes each primitive (line strip, line loop, triangle strip, triangle fan) as a separate glTF mesh primitive, defined in JSON and having a distinct indices accessor. The extension adds metadata allowing runtimes to stitch these primitives together as a single draw call using the primitive restart values, but the runtime must still download and parse data for the original N primitives.

Unfortunately... I think this approach is increasing file size a lot. For a U.S. Highways dataset (125K small polylines) GLB size increases from 8 MB to 38 MB after splitting each polyline into a separate primitive as the extension currently requires. I did reuse one POSITION attribute across all primitives. Draco and Meshopt compression don't help here, because much of the extra data is JSON.

I would ideally like to propose that EXT_mesh_primitive_restart should instead do what EXT_mesh_quantization does, and simply say "mesh primitives are allowed to use primitive restart values" now, without needing further metadata. If the runtime doesn't support primitive restart values, it would need to pre-process the index list. But this optimizes the extension for present-day and future use, where primitive restart support is standard, and imposes the cost of fallback only on the small (and shrinking) cases where primitive restart is not supported in hardware. I believe that's an important improvement for adoption and production use of the extension.

Draft samples

TODO

Draft implementations


Because the extension does not provide a way to specify fallback indices without restart values, files that use the extension must specify it in `extensionsRequired` array - the extension is not optional.

> **Implementation Note:** Implementations on graphics APIs without primitive restart may still support the extension, by rewriting primitive indices. Compared to processing many more primitives and accessors, the extension may still provide performance advantages even for these implementations, in assets with many primitives.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These changes led me to doing a little research into the landscape of primitive restart and where the GPUs and APIs currently land on it. The short and sweet of it is that the last device that doesn't support it was produced over a decade ago. With the exception of OpenGL SC (Safety Critical), OpenGL ES 2.0 and WebGL 1.0 everything supports primitive restart and implements it effectively the same way.

Of the 3 that don't support primitive restart, OpenGL SC is the only one that concerns me. I know some automotive manufacturers use glTF but I'm unsure if they use it with OpenGL SC. I would assume they're using some safety critical API because of ASIL, but whether that is Vulkan SC or OpenGL SC is unclear to me.

There's nothing to change here but I just wanted to add some context to what the landscape currently looks like.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's great to know, thanks — I hadn't looked beyond WebGL and WebGPU yet.

This extension permits the above prohibition to be selectively relaxed, while providing a trivial fallback for implementations that don't support primitive restart.
This extension removes the restriction above, allowing `indices` accessors to contain the maximum possible value for the component type in select primitive draw modes, and specifying that these values indicate primitive restart commands.

Because the extension does not provide a way to specify fallback indices without restart values, files that use the extension must specify it in `extensionsRequired` array - the extension is not optional.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Full disclosure: I am on the fence about the thoughts I've written below. I've been in standards long enough to know to avoid devil's advocacy, but I am still commenting here as I think we need to further discuss this. It may be overkill given the situation.

We should still allow (and encourage) supporting for fallback, despite what I discovered about the landscape of primitive restart. The complexity of glTF has continued to rise as 3DF evolves the standard.

This change would be the special case: the additional accessor metadata goes beyond the point where it begins to harm the ability to stream the glTF efficiently, so the glTF author makes it required and the additional metadata is no longer required.

Ultimately, we would like to eventually consider petitioning for ratification of this extension. A lack of a fallback may be a strategic issue from the 3DF perspective.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Understood that you're on the fence! A few comments for the record, just in case. Here, the fallback creates a "medicine worse than the disease" situation. Rewriting the indices as a runtime fallback is fairly cheap, and I think preferable to receiving separate primitives to begin with.

Certainly a different case than BasisU, Draco, or Meshopt compression, where an explicit fallback is necessary for any fallback to be available. And even in those cases (while I support making the fallback possible!), I would caution that I don't think these fallbacks are widely implemented today.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here, the fallback creates a "medicine worse than the disease" situation. Rewriting the indices as a runtime fallback is fairly cheap, and I think preferable to receiving separate primitives to begin with.

Yes, definitely. It may be worthwhile to add a non-normative section that goes through how to rewrite the indices at a high-level. While it may seem clear to you and me, it may not be clear to the dev who ends up implementing support for this extension in some obscure rendering engine.

Comment thread extensions/2.0/Vendor/EXT_mesh_primitive_restart/README.md
Comment thread extensions/2.0/Vendor/EXT_mesh_primitive_restart/README.md
[0, 1] [2, 3, 4]
```

This permits two equivalent representations of the geometry without duplicating any binary data. In glTF, the accessors look like this:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would it make sense to include an updated JSON example?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So, there's not really any JSON defined by the extension now — the glTF asset lists the extension under extensionsUsed and extensionsRequired, and the (binary) mesh primitive indices buffers are then allowed to contain primitive restart values (0xFF, 0xFFFF, or 0xFFFFFFFF). But the JSON defining the mesh primitives and their accessors is unchanged from the core spec.

Copy link
Copy Markdown
Collaborator

@lilleyse lilleyse left a comment

Choose a reason for hiding this comment

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

Looks good!

Comment thread extensions/2.0/Vendor/EXT_mesh_primitive_restart/README.md
@donmccurdy
Copy link
Copy Markdown
Member Author

donmccurdy commented Apr 15, 2026

Example dataset:

highway_roads.zip

The ZIP contains segments sampled from U.S. Highways in GeoJSON format, and three representations of the same file in glTF, with 125,000 LINE_STRIP primitives:

size file comments
11M highway_roads.json source GeoJSON
37M highway_roads_multiprim.glb 125,000 primitives
8.2M highway_roads.glb 1 primitive w/ primitive restart
2.1M highway_roads+meshopt.glb 1 primitive w/ primitive restart + meshopt

All three glTF models display correctly on https://gltf-viewer.donmccurdy.com/. three.js does not explicitly implement EXT_mesh_primitive_restart, but requires WebGL 2 and primitive restart is therefore on by default. The example is meant to demonstrate that the JSON required to represent each primitive separately is costly.

I didn't attempt to include EXT_mesh_primitive_restart in any of these samples, and just wrote the mesh primitive data as the extensions would have required. The files using primitive restart are therefore technically invalid. If there's a path forward for the extension, I'll create more sample files using EXT_mesh_primitive_restart properly.

@donmccurdy
Copy link
Copy Markdown
Member Author

@donmccurdy donmccurdy closed this Apr 16, 2026
@donmccurdy donmccurdy deleted the donmccurdy/EXT_mesh_primitive_restart branch April 20, 2026 23:29
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.

4 participants