Skip to content

Add KHR_mesh_primitive_restart - #2569

Open
donmccurdy wants to merge 7 commits into
KhronosGroup:mainfrom
CesiumGS:donmccurdy/KHR_mesh_primitive_restart
Open

Add KHR_mesh_primitive_restart#2569
donmccurdy wants to merge 7 commits into
KhronosGroup:mainfrom
CesiumGS:donmccurdy/KHR_mesh_primitive_restart

Conversation

@donmccurdy

@donmccurdy donmccurdy commented Apr 16, 2026

Copy link
Copy Markdown
Member

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.

Additional, simpler samples for line strip, line loop, triangle strip, and triangle fan topologies:

The line strip and line loop examples currently work in three.js, but not the triangle strip and triangle fan samples. All four work in PlayCanvas, shown below:

topology preview
line strip line-strip
line loop line-loop
triangle strip triangle-strip
triangle fan triangle-fan

Implementations

Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md Outdated
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md Outdated
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md Outdated
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md Outdated
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.

@donmccurdy

Copy link
Copy Markdown
Member Author

I've updated the PR description including simple test assets for all four topology types (line strip, line loop, triangle strip, triangle fan). Will address open comments on the PR next - thanks all!

donmccurdy and others added 2 commits June 24, 2026 10:18
Co-authored-by: Adam Morris <adam@cesium.com>
- Prefer 'restart index' to 'restart value'
- Clarify that maximum possible index values are still restricted for list topologies
- Minor cleanup
Comment thread extensions/2.0/Khronos/KHR_mesh_primitive_restart/README.md Outdated
> `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.
This extension modifies 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
Contributor

Choose a reason for hiding this comment

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

This is a potentially a nitpick, but it is better to use "relaxes" instead of "modifies" here. Modifies is a neutral term, whereas relaxes is a more technically precise term. Stating, "relaxes the restriction," helps to clearly establish the change in constraints adding to overall clarity.

@donmccurdy donmccurdy Jun 24, 2026

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 OK with me, so long as we think it's clear that "relaxes" does not imply removing the restriction entirely (list topologies are still subject to it). Personally I find "relaxes" to be clear in context. I'd also be fine with @lexaknyazev's earlier suggestion of "adjusts", but that's a similarly neutral term, so I'll let you both decide. :)

(For now, I've pushed a change to "relaxes")

@DRx3D DRx3D Jun 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@weegeekps : Copyright date should be the earliest date that the software is first publicly published (including distribution). It is allowed to include a range of years to indicate the most recent significant update. So something like: (c) 2025-2026 Bentley Systems, Incorporated. (or whatever is the correct years & owner). If there are multiple owners, then multiples lines are used.

@donmccurdy

Copy link
Copy Markdown
Member Author

In addition to the new samples linked above, I've pushed changes based on the suggestions and feedback. No intended changes in meaning or implementation, all changes are clarifications. I believe all outstanding feedback has been addressed, thanks @lexaknyazev and @weegeekps!


This extension relaxes 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 indices, assets that use the extension must specify it in `extensionsRequired` array - the extension is not optional.

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.

Would you consider relaxing "must specify" to "should specify" in extensionsRequired? If not, could the implementation note(s) at least acknowledge the tradeoffs below?

The mandate here creates an unusual failure mode: viewers that could render the content correctly refuse to load it. CesiumJS, for example, validates extensionsRequired against a whitelist and only added this extension in 1.142 (June 2026). Every earlier version will reject the glTF as containing an unsupported glTF extension, even though restart values would have rendered fine in CesiumJS powered by WebGL2 (WebGL 2 has PRIMITIVE_RESTART_FIXED_INDEX permanently enabled). We confirmed this through testing. The predecessor EXT was extensionsUsed-only, so those assets load everywhere.

The extensionsRequired mandate seems to be the correct classification (no fallback indices, like KHR_mesh_quantization). But unlike quantization, most unaware renderers on modern APIs can render restart properly. The tradeoff of making this required or not required seems to be receiving an error on capable viewers vs. rendering possible garbage on genuinely restart-less runtimes (mostly WebGL 1-era).

Back to "must specify" vs "should specify"...

"Should specify" phrasing would also preserve the guidance that assets really should list this in extensionsRequired by default. It just leaves room for an author with a good reason to deviate deliberately.

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.

The extension explicitly breaks the base glTF 2.0 spec so it can't be optional. Various pipeline tools may rightfully assume that every index value refers to a real vertex.

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 WebGL 1 based viewer would likely not display the assets correctly (except for special cases), and any spec-compliant renderer could, in theory, check the indices and bail out with an error if one of the invalid indices is found. (No renderer will do this, but more realisitically, one could imagine some geometry processing library manually accessing p = positions[indices[i]], causing an out-of-bounds access for the unexpected 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.

The extension explicitly breaks the base glTF 2.0 spec so it can't be optional.

Understood.

A brief note for implementers might still help. On APIs where restart is always enabled, supporting this extension may require no rendering implementation changes at all. Recognizing the extension name may suffice (CPU-side index consumers aside), as it did for CesiumJS. An implementation note might encourage runtimes to quickly declare support rather than reject assets they could already render.

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.

7 participants