Map previousWorld0-3 so instanced motion vectors are correct - #1839
Open
bkaradzic-microsoft wants to merge 3 commits into
Open
Conversation
A mesh rendered with object based motion blur (or prepass velocity) declares previousWorld0-3 alongside world0-3, so an instanced or thin-instanced mesh has 8 per-instance vec4 attributes, 9 with instanceColor. previousWorld0-3 had no built-in mapping, and VertexArray::RecordVertexBuffer rejected anything past the 5th instanced buffer, so those buffers were never recorded: the shader read a zero previous world matrix, produced a huge bogus velocity and smeared the whole object. This is what the nightly has been failing on since MRT support (BabylonJS#1754) turned object based motion blur on for Native. The i_data slots cannot come from a fixed per-name table. bgfx requires the used slots to form a contiguous run starting at i_data0: the D3D11 input layout declares TEXCOORD31 down to TEXCOORD(31 - N + 1) at 16-byte-dense offsets, and the GL path compacts the i_data locations it finds. Since the declared set varies (world0-3 alone, plus instanceColor, plus previousWorld0-3), a fixed table leaves a hole in the run and every attribute past the hole reads zero. So assign the slots per shader, from the set that shader actually declares, in reverse name order: the alphabetically first name gets the highest slot and the last gets i_data0. That is dense by construction, is stable between the base program compile and any instanced variant (the declared set is identical), and matches BuildInstanceDataBuffer, which packs the recorded buffers by descending attribute location. It also reproduces the previous fixed D3D table exactly for every set that existed before previousWorld0-3, so nothing else changes. OpenGL and Metal already assigned densely; they now share the same map instead of each counting attributes themselves. Raise the instanced buffer limit to bgfx's real BGFX_CONFIG_MAX_INSTANCE_DATA_COUNT (16, not the 5 its stale doc comment claims) and fix the off-by-one: the check ran before the insert, so it rejected the 5th buffer rather than the 6th. Playground validation, D3D11, Babylon.js 9.21.2: Thin instances + dynamic buffer resize 3.346% -> 2.478% Thin instances + render self motion blur 2.844% -> 2.355% Instances + render self motion blur 2.531% -> 2.061% all now under the 2.5% allowance, and the "Number of vertex buffer instances greater than 4 is not supported" errors go from 9 to 0. The numbers are bit-identical on hardware D3D11 and on forced WARP. A full 720 test A/B sweep shows no other test changing by more than 0.001%. A smaller residual difference remains on these three tests, from a separate bug: Babylon.js's bindAttachments is a no-op on Native, so the scene clear is applied to every MRT attachment and wipes the velocity attachment's alpha 0 background, leaving a faint halo around moving objects. That needs a matching Babylon.js change and is not addressed here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes incorrect instanced motion vectors by adding built-in shader compiler handling for previousWorld0-3 and ensuring per-instance i_data slots are assigned densely per shader (as required by bgfx). It also lifts the NativeEngine instanced-buffer recording limit to match bgfx’s actual instance-data slot capacity.
Changes:
- Add
previousWorld0-3as built-in instanced attributes and assign built-in instancei_dataslots per shader from the declared set (dense, stable mapping across variants). - Add/align compile-time guards for instance-data slot/location invariants and maximum slot counts.
- Increase the NativeEngine instanced vertex-buffer instance limit to
MAX_INSTANCE_DATA_SLOT_COUNTand fix the prior off-by-one behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp | Adds previousWorld0-3 built-in instance detection and introduces per-shader dense built-in i_data slot assignment used across GL/Metal/D3D traversers. |
| Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp | Updates static asserts/comments for the built-in instance-data location boundary and ensures built-in slot count doesn’t exceed max slot count. |
| Plugins/NativeEngine/Source/VertexArray.cpp | Raises instanced buffer instance limit to the bgfx max slot count and updates the thrown error message accordingly. |
| Plugins/NativeEngine/Source/NativeEngine.cpp | Updates documentation/comments for built-in instanced attribute rerouting behavior and the new built-in set. |
| Core/Graphics/InternalInclude/Babylon/Graphics/BgfxShaderInfo.h | Introduces MAX_INSTANCE_DATA_SLOT_COUNT, increases BUILTIN_INSTANCE_DATA_SLOT_COUNT to cover previousWorld0-3, and adjusts built-in location boundaries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
NativeEngine::Draw derives the routed location from the packing rank over every recorded instanced attribute, so it already accounts for built-ins. On OpenGL/Metal the built-ins are recorded at their stable, name-sorted locations, which puts them in the same range as consumer-declared ones, so a generic attribute sorting after a built-in shifts the packing and only the caller-supplied location reflects it. Prefer that location everywhere and exclude those attributes from the built-in slot assignment. On D3D built-ins carry synthetic locations at or above BUILTIN_INSTANCE_DATA_LAST_LOCATION and are never rerouted, so they never reach the map and nothing changes there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the nightly Playground validation failures on the three motion blur tests.
Problem
A mesh rendered with object based motion blur (or prepass velocity) declares
previousWorld0-3alongsideworld0-3, so an instanced or thin-instanced mesh has 8 per-instancevec4attributes, 9 withinstanceColor.previousWorld0-3had no built-in mapping in the shader compiler, andVertexArray::RecordVertexBufferrejected anything past the 5th instanced buffer (Number of vertex buffer instances greater than 4 is not supported). Those buffers were never recorded, the shader read a zero previous world matrix, and produced a huge bogus velocity that smeared the whole object.This is what the nightly has been failing on since MRT support (#1754) enabled object based motion blur on Native.
Why slots are now assigned per shader
The
i_dataslots cannot come from a fixed per-name table. bgfx requires the used slots to form a contiguous run starting ati_data0:TEXCOORD31down toTEXCOORD(31 - N + 1)at 16-byte-dense offsets;i_datalocations it finds.Since the declared set varies (
world0-3alone, plusinstanceColor, pluspreviousWorld0-3), a fixed table leaves a hole in the run and every attribute past the hole reads zero.So slots are assigned per shader from the set that shader actually declares, in reverse name order: alphabetically first gets the highest slot, last gets
i_data0. That is dense by construction, stable between the base program compile and any instanced variant (identical declared set), and matchesBuildInstanceDataBuffer, which packs the recorded buffers by descending attribute location.It also reproduces the previous fixed D3D table exactly for every set that existed before
previousWorld0-3(world0-3→i_data3..0,+instanceColor→i_data4,splatIndex0-3→i_data3..0), so nothing else changes. OpenGL and Metal already assigned densely; they now share the same map instead of each counting attributes themselves.The instanced buffer limit is raised to bgfx's real
BGFX_CONFIG_MAX_INSTANCE_DATA_COUNT(16 — its doc comment still claims 5), and the off-by-one is fixed: the check ran before the insert, so it rejected the 5th buffer rather than the 6th.Validation
Playground validation, Win32 D3D11, Babylon.js 9.21.2:
All now under the 2.5% allowance, and the instance-limit errors go from 9 to 0. The numbers are bit-identical on hardware D3D11 and on forced WARP, so they are not adapter-flaky.
A full 720 test A/B sweep shows no other test changing by more than 0.001%, and an identical set of pre-existing hangs on both builds.
Known remaining difference
A smaller residual diff remains on these three tests, from a separate bug: Babylon.js's
bindAttachmentsis a no-op on Native, so the scene clear is applied to every MRT attachment and wipes the velocity attachment's alpha-0 background. The motion blur shader multiplies velocity by that alpha, so the background gets a fixed non-zero velocity and moving objects get a faint halo. Fixing it needs a matching Babylon.js change plus per-attachment clear masking here, so it is not addressed in this PR.The worst test passes at 2.478% against a 2.5% allowance, so there is little headroom until that second bug is fixed.