fix: correct unordered access binding, storage image formats and subresource copies on Vulkan - #3311
fix: correct unordered access binding, storage image formats and subresource copies on Vulkan#3311sasvdw wants to merge 5 commits into
Conversation
Compute shaders binding an unordered access view produced wrong results on Vulkan, silently, for four reasons: - RWBuffer<T> mapped to VkDescriptorType.StorageImage, so the bind path cast the heap object to Texture, got null, and fell back to EmptyTexture. Now mapped to StorageTexelBuffer with the matching descriptor-write case. - Textures created with UnorderedAccess but no ShaderResource produced no image view and also bound EmptyTexture. The view usage is now composed from the flags the texture actually has rather than hardcoding Sampled. - Buffers and textures never advertised the compute pipeline stage, leaving uploads and readbacks unordered against a dispatch. Storage buffers also reported UniformRead instead of ShaderRead. - The SPIR-V emitter inferred a storage image format from the shader element type, but in HLSL the view determines the format. The inference is removed so the type's Unknown format flows through, as BufferType already did. Re-enables TestLambertPrefilteringSHPass2 and TestHammersley, blanket-skipped on Vulkan in 2020 as "compute shaders are not supported yet". Adds TestUnorderedAccessOnlyTexture, which reads back all zeros without these fixes. TestRadiancePrefilteringGgx keeps its Vulkan skip with an accurate reason: it exercises the non-compute prefilter and its Lavapipe output differs from the Direct3D reference at mip 1, which predates this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Looks good to me and makes sense, but as it touches parts of the shader compiler recently rewritten by Xen, I'd like if he also takes a look here.
Have you checked the resulting images? Apart from having 249+ differing pixels, do they look similar enough? I ask because for some tests, pixel difference counts are not a very good indicator, as little floating point differences between platforms may affect final colors a little bit, but enough to count as different pixel. |
|
I've compared it visually and have been trying to figure out if I can fix the one skipped level as it's looking like a miplevel missmatch that's leading to the 249+ differing pixels. |
CopyRegion accepted a source and a destination subresource index but ignored both. It addressed the copy with the texture MipLevel and ArraySlice instead. The extent still came from the index, so a copy of a non-zero mip read a correctly sized window out of mip 0. Direct3D11 and Direct3D12 pass both indices to the underlying API. Vulkan now decodes them the same way. Three callers pass a non-zero index: - RadiancePrefilteringGGX and its non-compute variant copy mip 2 of a 1024 cubemap into a 256 output, and read the top-left corner of mip 0 instead. - CubemapRendererBase addressed array layer 0 with a layerCount of 6 against a source with one layer. - VoxelStorageTextureClipmap landed every temporary mip on mip 0. This change re-enables TestRadiancePrefilteringGgx on Vulkan. The skip reason named Lavapipe, the non-compute path and mip 1, and all three were wrong. The difference also occurs on an NVIDIA GPU, on the compute path, and at mip 0. Mip 0 now matches the Direct3D reference with a maximum channel difference of 1, against 249 before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestHammersley and TestLambertPrefilteringSHPass2 lost their Vulkan skips earlier in this branch. TestUnorderedAccessOnlyTexture never had one. SkippableFact without a Skip call adds only indirection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Draft PR — automatic CI is skipped to save runner minutes.
|
The two removed XML parameters restated the parameter name. The storage image format comment now leads with the rule in active voice, because it exists to stop the inference from being added again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@Ethereal77, a close inspection with the CompareGold tool highlighted another bug I was able to pin down and fix where |
|
Good thing. I noticed in your images that the bottom and the back faces of the cubemap show a very noticeable seam. The filtering must be failing there somehow across that seam. Btw, I've had no opportunity yet to try the new gold image tests infrastructure (it has been recently revamped). I don't know if @xen2 wants reference images to be uploaded to this repo (probably not, as that would increase the LFS size by a lot) or some other place. |
|
@Ethereal77, the same seam exists in the D3D12 goldens, so likely a pre-existing issue if there is one: The seem is also present in the mip 0 golden: |
I'll await their feedback here. Didn't spot anything in the |
Indeed, I've found the seam in gold images all the way back to 2018! It is a bug in the filtered cubemap should be continuous across all its seams. That discontinuity is a high-frequency difference which the filter has failed to reduce. A filtered cubemap is "equivalent" to approximate the illumination function over the sphere. The prefiltered levels are just for approximating different roughness levels in a PBR pipeline. The cubemap faces is only a discretization of this sphere, so it must still be continuous. If not, visible artifacts may appear on rough surfaces. |
|
Please, can you create an issue for the prefiltering bug? I'm far from my main PC, and as you have recently reproduced the bug and have images to show it... |
|
@Ethereal77 I'll get a bug logged. Very likely I'll just have a look at fixing it as well 😄 |
PR Details
Compute shaders that bind an unordered access view produced wrong results on Vulkan. Five separate defects caused this, and every one of them failed silently. The shader wrote to a harmless place, and the reads returned empty data.
Three tests that were disabled because of these defects now run again.
1.
RWBuffer<T>bound as a storage imageConvertDescriptorTypemappedEffectParameterType.RWBuffertoVkDescriptorType.StorageImage. The bind path runsheapObject.Value as Texture, which returns null for aBuffer, and falls back toGraphicsDevice.EmptyTexture. Every dispatch wrote into a dummy image.The type now maps to
StorageTexelBuffer. The descriptor write joins the existingUniformTexelBuffercase, because both usepTexelBufferView. The two changes must land together, or thedefault:case throws.2. Unordered-access-only textures produced no image view
GetImageViewreturnedVkImageView.Nullunless the texture hadIsShaderResource. A texture created with onlyTextureFlags.UnorderedAccesstherefore also boundEmptyTexture.Relaxing that guard alone would add a second defect. The view usage was hardcoded to
Sampled, and such an image never carries that flag. The view usage now comes from the flags the texture actually has.3. Buffers and textures never advertised the compute stage
About twenty call sites pass
NativePipelineStageMasktovkCmdPipelineBarrier, but the mask listed only vertex and fragment stages. Uploads and readbacks had no dependency edge against a dispatch. Storage buffers also reportedVkAccessFlags.UniformReadinstead ofShaderRead. An unordered-access-only texture fell through every branch toVkPipelineStageFlags.None, which is zero and is not a legal stage mask.4. The SPIR-V emitter inferred a storage image format from the element type
GetStorageImageFormatmappedfloat4toRgba32f. In HLSL the view decides the format, not the element type.RWTexture2D<float4>binds equally toR8G8B8A8_UNormandR32G32B32A32_Float. The validation layer states the result plainly: "Any loads or stores with the variable will produce undefined values to the whole image."The emitter now lets the
Unknownformat flow through. This matchesBufferTypeand matches what DXC emits.ShaderMixeralready requestsStorageImage{Read,Write}WithoutFormatfor anUnknownformat, and the device already enables both features.5.
CopyRegionignored both subresource indicesThe method accepted
sourceSubresourceanddestinationSubResourcebut used neither. It addressed the copy with the texture MipLevel and ArraySlice, and with alayerCountofArraySize. Two// Review:comments in the file already recorded this. The copy extent still came from the index, so a copy of a non-zero mip read a correctly sized window out of mip 0. Direct3D11 and Direct3D12 pass both indices to the underlying API, and Vulkan now decodes them the same way.Three callers pass a non-zero index:
RadiancePrefilteringGGXand its non-compute variant copy mip 2 of a 1024 cubemap into a 256 output. They read the top-left corner of mip 0 instead.CubemapRendererBaseaddressed array layer 0 with alayerCountof 6 against a source that has one layer.VoxelStorageTextureClipmaplanded every temporary mip on mip 0.How the fifth defect was found
@Ethereal77 asked whether the images looked similar enough, because a count of differing pixels is a poor indicator on its own. That question found the defect.
The images did not look similar. Every cube face showed the top-left cell of a 4x4 grid, which is a 1024 to 256 crop. The original skip reason was wrong in three ways:
TestRadiancePrefilteringGgxnow runs on Vulkan with no skip. Mip 0 matches the Direct3D reference with a maximum channel difference of 1, against 249 before.Tests
TestLambertPrefilteringSHPass2andTestHammersleywere skipped on Vulkan in 2020 with the reason "compute shaders are not supported yet" (19085cb). That was a blanket disable, not a diagnosis. Both pass now.TestUnorderedAccessOnlyTextureis new. It writes dispatch coordinates into an unordered-access-only texture and asserts every texel. Without these fixes it reads back all zeros.Per @Ethereal77's review, a test with no skip condition now uses
[Fact]instead of[SkippableFact].Gold images
Vulkan output for the two filtered mip levels of
TestRadiancePrefilteringGgxdiffers from the Direct3D reference by more than the default tolerance. Thetest-gold-gen.ymlworkflow generated gold forWindows.Vulkan/LavapipeandmacOS.Vulkan/Apple M1. Linux, Android and iOS needed none, because the runtime fallback covers them. Mip 0 needed none on any platform, because it now matches the existing Direct3D gold.To answer the question about pixel counts directly, here is the divergence measured against a control that predates this work:
[16+]The Vulkan difference is uniform across all six cube faces, and it shows no correlation with luminance. This is rounding at the final 8-bit conversion, not a rendering difference. For contrast, mip 0 before the fix measured a maximum difference of 249, with 288,150 pixels in the
[16+]bucket.Verification
Debug build, so the validation layers load. Shader caches cleared between runs.
Stride.Graphics.Tests.11_0(6 tests)Stride.Graphics.Tests.10_0(48 tests)All three backends now report the same result. The remaining skip in 11_0 is
TestComputeShader, which has been disabled on every backend since 2018 and is untouched here.The 10_0 suite is included because the shader compiler change affects every RW texture in the engine. That covers the sixteen
LightingTestsand the material gold images, across the native SPIR-V path and the SPIRV-Cross to HLSL path.The GameStudio editor was also built and run from this branch, and the FPS sample renders correctly. On Windows the editor host uses Direct3D11, so this covers the shader compiler change through the SPIRV-Cross to HLSL path, including skybox prefiltering and PBR materials. It does not cover the four Vulkan defects, which the suites above cover.
One caveat, flagged because a reviewer may hit it:
TestHammersleyon Direct3D12 failed twice during this work. Both failures came on the first Direct3D12 run after another backend had run, and the signature was all 1024 sample points missing. It then passed 8 consecutive full-suite runs, 5 runs in isolation, and a run against cleared shader caches. I could not isolate a trigger, and I have not established whether it relates to the shader compiler change here.Notes
Two items found along the way. Both are out of scope, and each wants its own issue:
EffectCompilerCachekeys cached bytecode on the effect input hash, which excludes the shader compiler version. A compiler change silently reuses stale bytecode untilobj/stride/assetbuildand the per-targetbin/**/cachedirectories are cleared by hand.RadiancePrefilteringGGX, the compute prefilter, has no reference except a test that never enables it.Related Issue
Types of changes
Checklist