Skip to content

[Bugfix] Evaluate the model pass's surface attributes at the MSAA centroid - #1389

Merged
untoldengine merged 2 commits into
untoldengine:developfrom
miolabs:bugfix/msaa_centroid_attributes
Oct 28, 2026
Merged

untoldengine merged 2 commits into
untoldengine:developfrom
miolabs:bugfix/msaa_centroid_attributes

Conversation

@miogds

@miogds miogds commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

On the Vision Pro a smooth sphere lit by the sun from above showed sparse pale dots along its dark lower silhouette, over the passthrough, where a RealityKit sphere beside it showed none. The engine's eye texture rendered on the Mac has them: along the sphere's bottom arc the partly covered edge texels (a quarter or a half of the 4x MSAA samples) carried the colour of the sphere's sunlit top, up to 7.4 times as bright as the surface two texels in.

Under MSAA the model fragment runs once per pixel, at the pixel's centre, which along a silhouette lies outside the triangle: the world position, normal, tangent and uv it reads are extrapolated past the limb. On a smooth surface the extrapolation is small (the normal turns by the curvature times the distance past the limb, a degree or two here), but anything decided on those attributes is decided on a point that is not on the surface. The model pass's former back-face rule, judged on the interpolated normal, turned a limb normal a hair past 90° right round into the sun; develop judges the face itself since #1359, which is why the dots are gone from the same scene on develop while they remain on branches that predate it.

This branch gives the four surface attributes of VertexOutModel [[centroid_perspective]], so that the GPU evaluates them at the centroid of the covered samples, inside the triangle, for whatever is decided on them. The centroid of a fully covered pixel is its centre, so the opaque interior is unchanged, and without MSAA nothing changes. The velocity's two clip positions stay at the centre: both are extrapolated alike and their difference is the surface's motion either way. The [[position]] is Metal's own. No other stage-in struct shades from normals into the multisampled target (the transparency and gizmo shaders share VertexOutModel; the sky, grid, environment, outline, wireframe and light-visual structs do not light from normals).

The sphere's lower limb in the eye texture: develop at the pixel centre and at the centroid, and the former back-face rule at the pixel centre and at the centroid

The sheet is the eye texture around the sphere, magnified eightfold: the top row composited over a grey room by each texel's coverage (as a compositor shows it), the bottom row each drawn texel's straight colour (as it was shaded). From the left: develop with the attributes at the pixel centre, develop with this branch, the former back-face rule (the branch the headset ran) with the attributes at the pixel centre, which shows the pale texels along the dark bottom and the dark ones along the lit top, and the former rule with this branch's centroid, where both are gone.

Measured

The new test's scene: a slab, three cubes and a sphere of radius 6 cm at (0, 1.45, -1.6), the sun overhead at strength 3, drawn under 4x MSAA into the headset's eye texture (1920 × 1824, the presenter's frustum) from an eye at (0, 1.5, 0); the resolved lit colour is read before the look pass (linear, premultiplied by the coverage, the coverage in alpha). Along the sphere's bottom arc, 47 of its 56 columns have a partly covered edge texel over a fully covered texel two rows in; each edge texel's straight luminance is compared with that texel's.

shaders edge texels over 1.5x the texel two in the brightest
the former back-face rule, attributes at the pixel centre 13 of 47 (2.6–7.4x, straight colour ≈ (0.43, 0.58, 0.91) against (0.06–0.12)) 7.38x
the former rule, at the centroid 0 1.06x
develop, at the pixel centre 0 1.35x
develop with this branch 0 1.06x

Changes

  • Sources/UntoldEngine/Shaders/ShaderStructs.h: worldPosition, tbNormal, tangent and uvCoords of VertexOutModel carry [[centroid_perspective]], with the reason in the struct's comment.
  • The seven metallibs rebuilt with buildkernels.sh.
  • Tests/UntoldEngineRenderTests/MSAACentroidEdgeShadingTests.swift: the scene above; asserts that no partly covered edge texel along the sphere's dark bottom arc is over 1.5 times as bright as the surface two texels in (the former rule gives 13 of 47) and that the brightest is within 1.2 times (1.35 at the pixel centre, 1.06 at the centroid). It prints the sphere's rows, the columns read, the coverage steps seen, the mean straight colours of the edge and of the surface two in, every texel over the limit, and the worst. UNTOLD_MSAA_EDGE_CROP_DIR=<folder> makes it write the two crops of the sheet.

Verification

  • MSAACentroidEdgeShadingTests: fails on develop's kernels at the brightest edge texel (1.35 > 1.2), passes with the rebuilt kernels (1.06).
  • The full swift test run on the branch (an M-series Mac, Xcode 27):
    • UntoldEngineTests: 1727 tests, 3 skipped, 1 failure: ExternalRenderExtensionPackageTests, whose fixture package resolves the engine by its folder name and fails in any checkout not named UntoldEngine (this one is a worktree); unrelated.
    • UntoldEngineRenderTests: 974 tests passed through the suites up to StreamLodBatchFallbackTests, among them every suite that touches MSAA: MSAARenderingTests (6), PostFXTests (16; the MSAA, FXAA and SMAA PSNR tests pass against the existing references, so none was regenerated), HybridRenderPathTests (5), LookPresentFoldTests (2), OpaqueHiddenSurfaceRemovalTests (5), PerFragmentIndirectLightingTests (3), PerFragmentLightingTests (4), BackFaceFlipTests (2), RenderGraphBuilderTest (103), RenderExtensionResourceDescriptorTest (8) and the new test. 37 failures, all of one kind: the asset-loading drain in BaseRenderSetup.setUp timing out (15 s) in StaticBatchingTest and StreamLodBatchFallbackTests, an hour and a half into the process, with no rendering assertion among them; the run was stopped there. A stage-in qualifier cannot reach the asset loader.
    • The suites after that point that touch the opaque pass or XR, run on their own afterwards: TemporalAntiAliasingTests (6), TAAMovingObjectTests (2), TileLightStageTests (4), XRFoveationTests (3), XRImmersionContractTest (4), all passing.

…troid

Under MSAA the model fragment runs once per pixel, at the pixel's centre, which along a
silhouette lies outside the triangle: the world position, normal, tangent and uv it read
were extrapolated past the limb. On a smooth surface the error is small (the normal turns
by the curvature times the distance past the limb, a degree or two), but anything decided
on those attributes is decided on a point that is not on the surface. The pass's former
back-face rule, judged on the interpolated normal, turned a limb normal a hair past 90
degrees right round into the sun, and the partly covered edge texels along a sphere's dark
lower limb carried the colour of its sunlit top: 13 of 47 such texels 2.6 to 7.4 times as
bright as the surface two texels in, the sparse pale dots the Vision Pro showed along the
limb once the RealityKit presenter blended them by their coverage. The rule now judges the
face itself (untoldengine#1359); this gives the four attributes [[centroid_perspective]], so that they
are evaluated inside the covered samples for whatever is decided on them. On that arc the
brightest edge texel goes from 1.35 to 1.06 times the surface beside it; the fully covered
interior is unchanged (the centroid of a covered pixel is its centre) and without MSAA
nothing changes. The velocity's clip positions stay at the centre: both are extrapolated
alike and their difference is the surface's motion either way.

The kernels are rebuilt with buildkernels.sh. MSAACentroidEdgeShadingTests draws the
presenter's straightness scene (a slab, three cubes, a sphere, the sun overhead) into the
headset's eye texture under 4x MSAA from an eye 1.6 m before the sphere and reads its
bottom arc off the resolved lit colour: no partly covered edge texel over 1.5 times the
texel two rows in, and the brightest within 1.2 times.
@miogds
miogds requested a review from untoldengine as a code owner October 27, 2026 14:46
miogds pushed a commit to miolabs/UntoldEngine that referenced this pull request Oct 27, 2026
…ntroid change

The cherry-pick of untoldengine#1389 brought its shader source and test; its
metallibs were built from upstream develop and would have dropped this branch's look-pass
change (8554445), so the binaries kept this branch's and are rebuilt here from the sources
with both (`buildkernels.sh`).
miogds pushed a commit to miolabs/UntoldEngine that referenced this pull request Oct 27, 2026
…#1389) and on the presenter branch with the kernels rebuilt
@untoldengine
untoldengine merged commit c5b514c into untoldengine:develop Oct 28, 2026
3 of 4 checks passed
miogds added a commit to miolabs/UntoldEngine that referenced this pull request Oct 28, 2026
[Sync] develop with upstream: the model pass's surface attributes at the MSAA centroid (untoldengine#1389)
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.

2 participants