Skip to content

Add KHR_mesh_primitive_restart#2569

Open
donmccurdy wants to merge 3 commits intoKhronosGroup:mainfrom
CesiumGS:donmccurdy/KHR_mesh_primitive_restart
Open

Add KHR_mesh_primitive_restart#2569
donmccurdy wants to merge 3 commits intoKhronosGroup:mainfrom
CesiumGS:donmccurdy/KHR_mesh_primitive_restart

Conversation

@donmccurdy
Copy link
Copy Markdown
Contributor

@donmccurdy donmccurdy commented Apr 16, 2026

Motivation

glTF 2.0 includes seven primitive modes:

  • 0 POINTS
  • 1 LINES
  • 2 LINE_LOOP
  • 3 LINE_STRIP
  • 4 TRIANGLES
  • 5 TRIANGLE_STRIP
  • 6 TRIANGLE_FAN

Four of these (LINE_LOOP, LINE_STRIP, TRIANGLE_STRIP, TRIANGLE_FAN) define topologies that may contain any number of vertices. Each glTF mesh primitive defines a single contiguous topology for these types, because primitive restart values — which would instruct the graphics API to begin a new topological primitive, without a new draw call — are disallowed by the core specification for maximum compatibility (see: WebGL 1).

This PR proposes a KHR_mesh_primitive_restart extension, which can be added to any glTF asset, removing the restriction against primitive restart values. The mechanism and implementation are intentionally very similar to KHR_mesh_quantization, which removes restrictions on particular vertex attribute component types.

Primitive restart values allow a scene to contain arbitrary numbers of line loops, line strips, triangle strips, and triangle fans, without requiring arbitrary numbers of mesh primitives, along with costly processing and draw calls. This also greatly reduces the amount of JSON (primitive and accessor) definitions required to represent such scenes.

Background

The existing EXT_mesh_primitive_restart allows primitive restart values, but does so by encoding each primitive topology as a separate glTF mesh primitive, defined in JSON and having a distinct indices accessor. The extension adds indices with restart values as metadata, allowing runtimes to stitch these primitives together as a single draw call, but the runtime must still download and parse data for the original N primitives. Unfortunately, the performance implications of this approach are not ideal. See CesiumGS#100.

EXT vs. KHR

I'm open to modifying the existing EXT_mesh_primitive_restart extension, but modifying an existing EXT extension would be an unusual step. While I believe it has only very limited implementations today, initial feedback has been on the side of avoiding changes to an existing EXT extension.

For that reason, and hoping the approach here might be broadly useful for anyone using these primitive topologies in glTF 2.0, I'm proposing a simplified/optimized approach as a new extension, KHR_mesh_primitive_restart.

Fallback

If a 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.

Fallback may not necessarily require separate draw calls for each topological primitive. Multi-draw Indirect (MDI) would be an alternative (if there exists a graphic API that supports MDI but not primitive restart values). I'm not familiar with how a path-tracing renderer would want to handle primitive restart indices.

Samples

PrimitiveRestartLineStrip.glb.zip

hilbert3d

Figure: A Hilbert space-filling curve, rendered in 3D. The asset is structured as a glTF scene and a single mesh primitive, containing a series of LINE_STRIP topologies separated by primitive restart values. Created with glTF Transform.

In some viewers (including https://gltf-viewer.donmccurdy.com/) the model will display correctly without any modifications to support KHR_mesh_primitive_restart, because primitive restart is already enabled by default by the WebGL 2 API.

Implementations


Because the extension does not provide a way to specify fallback indices without restart values, assets 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 a larger number of primitives and accessors, the extension may still provide performance advantages even for these implementations.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
> **Implementation Note:** Implementations on graphics APIs without primitive restart may still support the extension, by rewriting primitive indices. Compared to processing a larger number of primitives and accessors, the extension may still provide performance advantages even for these implementations.
> [!NOTE]
> Implementations on graphics APIs without primitive restart may still support the extension, by rewriting primitive indices. Compared to processing a larger number of primitives and accessors, the extension may still provide performance advantages even for these implementations.


| `accessor.componentType` | restart value |
| ---------------------------- | ------------- |
| `5121` (UNSIGNED_BYTE) | `255` |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably need to provide some guidance on how to handle cases where the accessor component type chosen is not supported by the API or engine.

This will be an issue that the WebGPU implementations will likely run into if the creator of the glTF uses UNSIGNED_BYTE. WebGPU's implementation of primitive restart only supports uint16 and uint32.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't it a glTF 2.0 core issue? This extension does not add any new index data types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, it is a core issue in glTF 2.0. My line of thinking here is that it might be worthwhile to provide an informative guidance to point people into the right direction, but in retrospect it probably isn't necessary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If anything, we might add a note about 8-bit indices to the main spec someday (just stating the obvious: convert to 16-bit if unsupported) but there's nothing to do about that in this extension.


The applicable primitive restart value is determined by the accessor component type:

| `accessor.componentType` | restart value |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A minor nitpick:

Suggested change
| `accessor.componentType` | restart value |
| `accessor.componentType` | restart index |

Much of the existing standards and literature refers to these values as the "restart index" and it is probably good if we use follow that.


> **Implementation Note:** Implementations on graphics APIs without primitive restart may still support the extension, by rewriting primitive indices. Compared to processing a larger number of primitives and accessors, the extension may still provide performance advantages even for these implementations.

## Extending Mesh Indices
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to include a normative statement in this section that explicitly states that if an engine uses a different index than the ones specified, implementers are responsible for the remapping?

Both OpenGL and Vulkan support setting an arbitrary value as the primitive restart index. OpenGL through the glPrimitiveRestartIndex() function, and Vulkan through the vkCmdSetPrimitiveRestartIndex() function provided in the VK_EXT_primitive_restart_index extension, which was promoted to core in Vulkan 1.3.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if an engine uses a different index than the ones specified

It's out of the spec's scope. The file format spec defines what's stored in the file.

Besides, arbitrary restart indices are not widely supported and the mentioned Vulkan extension exists only for legacy emulation purposes; it is about one month old by the way.


> `indices` accessor **MUST NOT** contain the maximum possible value for the component type used (i.e., 255 for unsigned bytes, 65535 for unsigned shorts, 4294967295 for unsigned ints).

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please change "removes" to "adjusts" (or similar) because the restriction must remain for list topologies.

@@ -0,0 +1,76 @@
<!--
Copyright 2025 Bentley Systems, Incorporated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2026

This was referenced Apr 30, 2026
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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