fix: read by direction when resampling the prefiltered radiance highest level - #3341
Open
sasvdw wants to merge 3 commits into
Open
fix: read by direction when resampling the prefiltered radiance highest level#3341sasvdw wants to merge 3 commits into
sasvdw wants to merge 3 commits into
Conversation
Ethereal77
reviewed
Aug 9, 2026
Comment on lines
+127
to
+130
| [SkippableFact] | ||
| public void HighestLevelIsContinuousAcrossFaces() | ||
| { | ||
| SkipTestForGraphicPlatform(GraphicsPlatform.Vulkan); |
Contributor
There was a problem hiding this comment.
Why is this skipped for Vulkan?
Contributor
Author
There was a problem hiding this comment.
It was a bad copy on my end. Thought I was still working with the code from another branch where I had the fixes living, for skipping some related tests and ended up duplicating the test suite without a proper look.
Fails today. RadiancePrefilteringGGXNoCompute fills mip 0 with a per-face ImageScaler rescale when it cannot take its copy shortcut. That rescale reads a single face view and clamps at the face border, so the border texels never see the neighboring face. The test drives the filter with an output format that differs from the source, which is the condition SkyboxGenerator creates for a low dynamic range skybox. It then measures the step across the 12 shared face edges against the step between neighboring texels inside a face. Mip 0 measures 0.86, while the levels the filter produces stay near 0.32. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st level RadiancePrefilteringGGXNoCompute fills mip 0 from the source when DoNotFilterHighestLevel is set. A straight copy serves that only while the sizes, the mip count and the format all line up. Everything else went through ImageScaler over a single face view, which clamps at the face border, so the border texels never saw the neighboring face. SkyboxGenerator creates exactly that case for a low dynamic range skybox, because it fixes the filtering format at R8G8B8A8_UNorm while the source compiles to a block format. The filter cannot serve this level either. At roughness 0 the GGX distribution gives a zero probability density, the solid angle goes to infinity and the sample mip clamps to the smallest level, so every texel returns one averaged color. CubemapFaceResampleShader reads the source by direction instead, which crosses face boundaries the way the hardware samples a cubemap. Measured over the 12 shared face edges, against the step between neighboring texels inside a face, the level goes from 0.86 to 0.11 and keeps its detail. No gold image moves. Every cubemap in the tree takes the copy path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The test carried SkipTestForGraphicPlatform(Vulkan), copied from the neighboring test without a reason of its own. stride3d#3311 removed the Vulkan skips from this suite, including the one on TestRadiancePrefilteringGgx, so keeping this one puts back the pattern that work removed. Nothing here is backend specific. The test drives the pixel shader path, writes a render target cubemap array, reads it back and asserts on numbers. It takes no screenshot, so no gold image is involved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sasvdw
force-pushed
the
fix/ggx-prefilter-mip0-per-face-rescale
branch
from
August 9, 2026 19:48
5ceebdc to
8267444
Compare
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.
PR Details
Summary
Mip 0 of the prefiltered cubemap lost continuity around every face edge whenever the copy shortcut did not
apply, because the fallback rescaled each face on its own.
Description
RadiancePrefilteringGGXNoComputefills mip 0 from the source whenDoNotFilterHighestLevelis set. Thecopy at
:103serves that only while the sizes, the mip count and the format agree. Everything else wentthrough
ImageScalerover a single face view, which clamps at the face border, so the border texels neversaw the neighboring face.
This replaces that fallback with
CubemapFaceResampleShader, which reads the source by direction. Aborder texel then crosses the face edge the way the hardware samples a cubemap.
Falling through to the existing filter would look like the tidier fix, and it is what the compute path
appears to do, but it does not work. At roughness 0 the GGX distribution returns a zero probability
density, the solid angle goes to infinity, and the sample mip clamps to
MipmapCount(
RadiancePrefilteringGGXNoComputeShader.sdsl:41-44). Every texel reads the smallest mipmap and the levelbecomes one flat color. That is why mip 0 carries a special case at all.
Measured over the 12 shared face edges, against the step between neighboring texels inside a face:
ImageScalerThe middle row is the trap. A flat level reports no seam, so a continuity check alone would call it fixed.
The test asserts the level still carries an image for that reason.
Motivation and context — see #3334.
Related Issue
Fixes #3334.
Types of changes
Checklist
Validation status
TestRadiancePrefilteringGgxFaceContinuitydrives the filter with an output format that differsfrom the source, which is the case
SkyboxGeneratorcreates for a low dynamic range skybox. It measuresthe step across the 12 shared face edges and asserts the level still carries an image. Red at 0.86 before
the change, green at 0.11 after, verified both ways.
reaches the changed branch.
Stride.Graphics.Tests.11_0passes 5 of 5, includingTestRadiancePrefilteringGgx. The skybox cases inStride.Graphics.Tests.10_0pass 3 of 3.that path also skips the format check the no-compute path makes. Both are described in the issue and left
alone here.