Repository: stride3d/stride
Pull request #2684: fix: Vulkan concurrency and validation error fixes
Author: login: johang88
Labels: name: area-Graphics
Created: 2025-03-22T08:13:07Z | Merged: 2025-03-29T15:51:09Z

# PR Details

* Fixed concurrency issues that caused a full driver crash when texture streaming is enabled.
* Fixed a validation error casued by the default `NoSampler` that is used when converting `Texture.Load` to `texelFetch`.

## Related Issue

Found some validation errors when running in debug mode and also full driver crash when texture streaming was enabled.

## Types of changes

- [ ] Docs change / refactoring / dependency upgrade
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist

- [ ] My change requires a change to the documentation.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] **I have built and run the editor to try this change out.**



Diff:
diff --git a/sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs
index ed3e433b40..c8e1acbcce 100644
--- a/sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs
+++ b/sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs
@@ -40,6 +40,7 @@ public partial class GraphicsDevice
         private IntPtr nativeUploadBufferStart;
         private int nativeUploadBufferSize;
         private int nativeUploadBufferOffset;
+        private object nativeUploadBufferLock = new();
 
         private Queue<KeyValuePair<long, VkFence>> nativeFences = new Queue<KeyValuePair<long, VkFence>>();
         private long lastCompletedFence;
@@ -218,7 +219,11 @@ public unsafe void ExecuteCommandLists(int count, CompiledCommandList[] commandL
                 pWaitSemaphores = &presentSemaphoreCopy,
                 pWaitDstStageMask = &pipelineStageFlags,
             };
-            vkQueueSubmit(NativeCommandQueue, 1, &submitInfo, fence);
+
+            lock (QueueLock)
+            {
+                vkQueueSubmit(NativeCommandQueue, 1, &submitInfo, fence);
+            }
 
             presentSemaphore = VkSemaphore.Null;
             nativeResourceCollector.Release();
@@ -366,41 +371,44 @@ private unsafe void InitializePlatformDevice(GraphicsProfile[] graphicsProfiles,
 
         internal unsafe IntPtr AllocateUploadBuffer(int size, out VkBuffer resource, out int offset)
         {
-            // TODO D3D12 thread safety, should we simply use locks?
-            if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
+            lock (nativeUploadBufferLock)
             {
-                if (nativeUploadBuffer != VkBuffer.Null)
+                if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
                 {
-                    vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
-                    Collect(nativeUploadBuffer);
-                    Collect(nativeUploadBufferMemory);
-                }
+                    if (nativeUploadBuffer != VkBuffer.Null)
+                    {
+                        vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
+                        Collect(nativeUploadBuffer);
+                        Collect(nativeUploadBufferMemory);
+                    }
 
-                // Allocate new buffer
-                // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
-                // TODO D3D12 ResourceStates.CopySource not working?
-                nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
+                    // Allocate new buffer
+                    // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
+                    // TODO D3D12 ResourceStates.CopySource not working?
+                    nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
 
-                var bufferCreateInfo = new VkBufferCreateInfo
-                {
-                    sType = VkStructureType.BufferCreateInfo,
-                    size = (ulong)nativeUploadBufferSize,
-                    flags = VkBufferCreateFlags.None,
-                    usage = VkBufferUsageFlags.TransferSrc,
-                };
-                vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
-                AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
+                    var bufferCreateInfo = new VkBufferCreateInfo
+                    {
+                        sType = VkStructureType.BufferCreateInfo,
+                        size = (ulong)nativeUploadBufferSize,
+                        flags = VkBufferCreateFlags.None,
+                        usage = VkBufferUsageFlags.TransferSrc,
+                    };
+                    vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
+                    AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
 
-                fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
-                    vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
-                nativeUploadBufferOffset = 0;
-            }
+                    fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
+                        vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
+                    nativeUploadBufferOffset = 0;
+                }
+
+                // Bump allocate
+                resource = nativeUploadBuffer;
+                offset = nativeUploadBufferOffset;
+                nativeUploadBufferOffset += size;
 
-            // Bump allocate
-            resource = nativeUploadBuffer;
-            offset = nativeUploadBufferOffset;
-            nativeUploadBufferOffset += size;
-            return nativeUploadBufferStart + offset;
+                return nativeUploadBufferStart + offset;
+            }
         }
 
         protected unsafe void AllocateMemory(VkMemoryPropertyFlags memoryProperties)
@@ -523,7 +531,11 @@ internal unsafe long ExecuteCommandListInternal(CompiledCommandList commandList)
                 pWaitSemaphores = &presentSemaphoreCopy,
                 pWaitDstStageMask = &pipelineStageFlags,
             };
-            vkQueueSubmit(NativeCommandQueue, 1, &submitInfo, fence);
+
+            lock (QueueLock)
+            {
+                vkQueueSubmit(NativeCommandQueue, 1, &submitInfo, fence);
+            }
 
             presentSemaphore = VkSemaphore.Null;
             nativeResourceCollector.Release();
diff --git a/sources/engine/Stride.Shaders.Compiler/OpenGL/ShaderCompiler.cs b/sources/engine/Stride.Shaders.Compiler/OpenGL/ShaderCompiler.cs
index c4d4345885..b896e65bb3 100644
--- a/sources/engine/Stride.Shaders.Compiler/OpenGL/ShaderCompiler.cs
+++ b/sources/engine/Stride.Shaders.Compiler/OpenGL/ShaderCompiler.cs
@@ -296,6 +296,9 @@ private string Compile(string shaderSource, string entryPoint, ShaderStage stage
                     var noSampler = new EffectResourceBindingDescription { KeyInfo = { KeyName = "NoSampler" }, RawName = "NoSampler", Class = EffectParameterClass.Sampler, SlotStart = -1, SlotCount = 1 };
                     reflection.ResourceBindings.Add(noSampler);
 
+                    // Make sure it's a point sampler as some texture formats do not support linear sampling which will result in validation errors.
+                    reflection.SamplerStates.Add(new EffectSamplerStateBinding("NoSampler", new SamplerStateDescription(TextureFilter.Point, TextureAddressMode.Clamp)));
+
                     // Defines the ordering of resource groups in Vulkan. This is mirrored in the PipelineState
                     var resourceGroups = reflection.ResourceBindings.Select(x => x.ResourceGroup ?? "Globals").Distinct().ToList();
 


Reviews:
user: login: xen2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T09:12:28Z
user: login: johang88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T10:12:59Z

Review comments:
diff_hunk: @@ -366,40 +371,43 @@ private unsafe void InitializePlatformDevice(GraphicsProfile[] graphicsProfiles,
 
         internal unsafe IntPtr AllocateUploadBuffer(int size, out VkBuffer resource, out int offset)
         {
-            // TODO D3D12 thread safety, should we simply use locks?
-            if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
+            lock (nativeUploadBufferLock)
             {
-                if (nativeUploadBuffer != VkBuffer.Null)
+                if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
                 {
-                    vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
-                    Collect(nativeUploadBuffer);
-                    Collect(nativeUploadBufferMemory);
-                }
+                    if (nativeUploadBuffer != VkBuffer.Null)
+                    {
+                        vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
+                        Collect(nativeUploadBuffer);
+                        Collect(nativeUploadBufferMemory);
+                    }
 
-                // Allocate new buffer
-                // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
-                // TODO D3D12 ResourceStates.CopySource not working?
-                nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
+                    // Allocate new buffer
+                    // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
+                    // TODO D3D12 ResourceStates.CopySource not working?
+                    nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
 
-                var bufferCreateInfo = new VkBufferCreateInfo
-                {
-                    sType = VkStructureType.BufferCreateInfo,
-                    size = (ulong)nativeUploadBufferSize,
-                    flags = VkBufferCreateFlags.None,
-                    usage = VkBufferUsageFlags.TransferSrc,
-                };
-                vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
-                AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
+                    var bufferCreateInfo = new VkBufferCreateInfo
+                    {
+                        sType = VkStructureType.BufferCreateInfo,
+                        size = (ulong)nativeUploadBufferSize,
+                        flags = VkBufferCreateFlags.None,
+                        usage = VkBufferUsageFlags.TransferSrc,
+                    };
+                    vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
+                    AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
+
+                    fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
+                        vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
+                    nativeUploadBufferOffset = 0;
+                }
 
-                fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
-                    vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
-                nativeUploadBufferOffset = 0;
+                // Bump allocate
+                resource = nativeUploadBuffer;
+                offset = nativeUploadBufferOffset;
+                nativeUploadBufferOffset += size;
             }
 
-            // Bump allocate
-            resource = nativeUploadBuffer;
-            offset = nativeUploadBufferOffset;
-            nativeUploadBufferOffset += size;
             return nativeUploadBufferStart + offset;
path: sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs
user: login: xen2
body: Shouldn't a copy of `nativeUploadBufferStart` be made inside the lock?
created_at: 2025-03-22T09:11:54Z
author_association: MEMBER
diff_hunk: @@ -366,40 +371,43 @@ private unsafe void InitializePlatformDevice(GraphicsProfile[] graphicsProfiles,
 
         internal unsafe IntPtr AllocateUploadBuffer(int size, out VkBuffer resource, out int offset)
         {
-            // TODO D3D12 thread safety, should we simply use locks?
-            if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
+            lock (nativeUploadBufferLock)
             {
-                if (nativeUploadBuffer != VkBuffer.Null)
+                if (nativeUploadBuffer == VkBuffer.Null || nativeUploadBufferOffset + size > nativeUploadBufferSize)
                 {
-                    vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
-                    Collect(nativeUploadBuffer);
-                    Collect(nativeUploadBufferMemory);
-                }
+                    if (nativeUploadBuffer != VkBuffer.Null)
+                    {
+                        vkUnmapMemory(NativeDevice, nativeUploadBufferMemory);
+                        Collect(nativeUploadBuffer);
+                        Collect(nativeUploadBufferMemory);
+                    }
 
-                // Allocate new buffer
-                // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
-                // TODO D3D12 ResourceStates.CopySource not working?
-                nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
+                    // Allocate new buffer
+                    // TODO D3D12 recycle old ones (using fences to know when GPU is done with them)
+                    // TODO D3D12 ResourceStates.CopySource not working?
+                    nativeUploadBufferSize = Math.Max(4 * 1024 * 1024, size);
 
-                var bufferCreateInfo = new VkBufferCreateInfo
-                {
-                    sType = VkStructureType.BufferCreateInfo,
-                    size = (ulong)nativeUploadBufferSize,
-                    flags = VkBufferCreateFlags.None,
-                    usage = VkBufferUsageFlags.TransferSrc,
-                };
-                vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
-                AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
+                    var bufferCreateInfo = new VkBufferCreateInfo
+                    {
+                        sType = VkStructureType.BufferCreateInfo,
+                        size = (ulong)nativeUploadBufferSize,
+                        flags = VkBufferCreateFlags.None,
+                        usage = VkBufferUsageFlags.TransferSrc,
+                    };
+                    vkCreateBuffer(NativeDevice, &bufferCreateInfo, null, out nativeUploadBuffer);
+                    AllocateMemory(VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
+
+                    fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
+                        vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
+                    nativeUploadBufferOffset = 0;
+                }
 
-                fixed (IntPtr* nativeUploadBufferStartPtr = &nativeUploadBufferStart)
-                    vkMapMemory(NativeDevice, nativeUploadBufferMemory, 0, (ulong)nativeUploadBufferSize, VkMemoryMapFlags.None, (void**)nativeUploadBufferStartPtr);
-                nativeUploadBufferOffset = 0;
+                // Bump allocate
+                resource = nativeUploadBuffer;
+                offset = nativeUploadBufferOffset;
+                nativeUploadBufferOffset += size;
             }
 
-            // Bump allocate
-            resource = nativeUploadBuffer;
-            offset = nativeUploadBufferOffset;
-            nativeUploadBufferOffset += size;
             return nativeUploadBufferStart + offset;
path: sources/engine/Stride.Graphics/Vulkan/GraphicsDevice.Vulkan.cs
user: login: johang88
body: makes sense, I'll move the return inside the lock and that should take care of it.
created_at: 2025-03-22T10:12:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: johang88
created_at: 2025-03-22T08:15:49Z
body: These changes allowed me to run the default hdr new game sample in vulkan with no crashes, which is an improvement! There are probably more issues that need fixing but these should be mergable.
author_association: CONTRIBUTOR
user: login: Eideren
created_at: 2025-03-29T15:51:15Z
body: Thanks !
author_association: COLLABORATOR