From c371ec2a59d1d8503c4b193619ae44ccabbf0f7d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Jul 2023 05:39:14 +0900 Subject: [PATCH 01/18] Initial implementation of SSBOs for GL and Veldrid --- osu.Framework/FrameworkEnvironment.cs | 3 + .../Buffers/GLShaderStorageBufferObject.cs | 96 +++++++++ .../Buffers/IGLShaderStorageBufferObject.cs | 9 + osu.Framework/Graphics/OpenGL/GLRenderer.cs | 14 +- .../Graphics/Rendering/Dummy/DummyRenderer.cs | 3 + .../Dummy/DummyShaderStorageBufferObject.cs | 28 +++ osu.Framework/Graphics/Rendering/IRenderer.cs | 19 ++ .../Rendering/IShaderStorageBufferObject.cs | 25 +++ osu.Framework/Graphics/Rendering/Renderer.cs | 19 +- .../ShaderStorageBufferObjectStack.cs | 184 ++++++++++++++++++ .../VeldridShaderStorageBufferObject.cs | 100 ++++++++++ .../Graphics/Veldrid/VeldridRenderer.cs | 5 + 12 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs create mode 100644 osu.Framework/Graphics/OpenGL/Buffers/IGLShaderStorageBufferObject.cs create mode 100644 osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs create mode 100644 osu.Framework/Graphics/Rendering/IShaderStorageBufferObject.cs create mode 100644 osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs create mode 100644 osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs diff --git a/osu.Framework/FrameworkEnvironment.cs b/osu.Framework/FrameworkEnvironment.cs index 091dc50fb3..9bb0e43f48 100644 --- a/osu.Framework/FrameworkEnvironment.cs +++ b/osu.Framework/FrameworkEnvironment.cs @@ -16,6 +16,7 @@ public static class FrameworkEnvironment public static string? PreferredGraphicsRenderer { get; } public static int? StagingBufferType { get; } public static int? VertexBufferCount { get; } + public static bool NoStructuredBuffers { get; } static FrameworkEnvironment() { @@ -31,6 +32,8 @@ static FrameworkEnvironment() if (int.TryParse(Environment.GetEnvironmentVariable("OSU_GRAPHICS_STAGING_BUFFER_TYPE"), out int stagingBufferImplementation)) StagingBufferType = stagingBufferImplementation; + + NoStructuredBuffers = Environment.GetEnvironmentVariable("OSU_GRAPHICS_NO_SSBO") == "1"; } } } diff --git a/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs new file mode 100644 index 0000000000..d262fc3140 --- /dev/null +++ b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Rendering; +using osuTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + internal class GLShaderStorageBufferObject : IShaderStorageBufferObject, IGLShaderStorageBufferObject + where TData : unmanaged, IEquatable + { + public int Size { get; } + + public int Id { get; } + + private readonly TData[] data; + private readonly GLRenderer renderer; + private readonly uint elementSize; + + public GLShaderStorageBufferObject(GLRenderer renderer, int uboSize, int ssboSize) + { + this.renderer = renderer; + + Id = GL.GenBuffer(); + Size = renderer.UseStructuredBuffers ? ssboSize : uboSize; + data = new TData[Size]; + elementSize = (uint)Marshal.SizeOf(default(TData)); + + GL.BindBuffer(BufferTarget.UniformBuffer, Id); + GL.BufferData(BufferTarget.UniformBuffer, (IntPtr)(elementSize * Size), ref data[0], BufferUsageHint.DynamicDraw); + GL.BindBuffer(BufferTarget.UniformBuffer, 0); + } + + private int changeBeginIndex = -1; + private int changeCount; + + public TData this[int index] + { + get => data[index]; + set + { + if (data[index].Equals(value)) + return; + + data[index] = value; + + if (changeBeginIndex == -1) + { + // If this is the first upload, nothing more needs to be done. + changeBeginIndex = index; + } + else + { + // If this is not the first upload, then we need to check if this index is contiguous with the previous changes. + if (index != changeBeginIndex + changeCount) + { + // This index is not contiguous. Flush the current uploads and start a new change set. + Flush(); + changeBeginIndex = index; + } + } + + changeCount++; + } + } + + public void Flush() + { + if (changeBeginIndex == -1) + return; + + if (renderer.UseStructuredBuffers) + { + GL.BindBuffer(BufferTarget.UniformBuffer, Id); + GL.BufferSubData(BufferTarget.UniformBuffer, (IntPtr)(changeBeginIndex * elementSize), (IntPtr)(elementSize * changeCount), ref data[changeBeginIndex]); + GL.BindBuffer(BufferTarget.UniformBuffer, 0); + } + else + { + GL.BindBuffer(BufferTarget.UniformBuffer, Id); + GL.BufferSubData(BufferTarget.UniformBuffer, (IntPtr)(changeBeginIndex * elementSize), (IntPtr)(elementSize * changeCount), ref data[changeBeginIndex]); + GL.BindBuffer(BufferTarget.UniformBuffer, 0); + } + + changeBeginIndex = -1; + changeCount = 0; + } + + public void Dispose() + { + GL.DeleteBuffer(Id); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/IGLShaderStorageBufferObject.cs b/osu.Framework/Graphics/OpenGL/Buffers/IGLShaderStorageBufferObject.cs new file mode 100644 index 0000000000..c563811321 --- /dev/null +++ b/osu.Framework/Graphics/OpenGL/Buffers/IGLShaderStorageBufferObject.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + internal interface IGLShaderStorageBufferObject : IGLUniformBuffer + { + } +} diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 68f965de4b..5262ca2970 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -44,6 +44,8 @@ protected internal override bool VerticalSync public override bool IsUvOriginTopLeft => false; public override bool IsClipSpaceYInverted => false; + public bool UseStructuredBuffers { get; private set; } + /// /// The maximum allowed render buffer size. /// @@ -80,12 +82,16 @@ protected override void Initialise(IGraphicsSurface graphicsSurface) GL.Disable(EnableCap.StencilTest); GL.Enable(EnableCap.Blend); + string extensions = GetExtensions(); + Logger.Log($@"GL Initialized GL Version: {GL.GetString(StringName.Version)} GL Renderer: {GL.GetString(StringName.Renderer)} GL Shader Language version: {GL.GetString(StringName.ShadingLanguageVersion)} GL Vendor: {GL.GetString(StringName.Vendor)} - GL Extensions: {GetExtensions()}"); + GL Extensions: {extensions}"); + + UseStructuredBuffers = extensions.Contains(@"GL_ARB_shader_storage_buffer_object") && !FrameworkEnvironment.NoStructuredBuffers; openGLSurface.ClearCurrent(); } @@ -431,7 +437,11 @@ public override IFrameBuffer CreateFrameBuffer(RenderBufferFormat[]? renderBuffe return new GLFrameBuffer(this, glFormats, glFilteringMode); } - protected override IUniformBuffer CreateUniformBuffer() => new GLUniformBuffer(this); + protected override IUniformBuffer CreateUniformBuffer() + => new GLUniformBuffer(this); + + protected override IShaderStorageBufferObject CreateShaderStorageBufferObject(int uboSize, int ssboSize) + => new GLShaderStorageBufferObject(this, uboSize, ssboSize); protected override INativeTexture CreateNativeTexture(int width, int height, bool manualMipmaps = false, TextureFilteringMode filteringMode = TextureFilteringMode.Linear, Color4? initialisationColour = null) diff --git a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs index 41649e2727..f94221b240 100644 --- a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs @@ -213,6 +213,9 @@ public IVertexBatch CreateQuadBatch(int size, int maxBuffers) public IUniformBuffer CreateUniformBuffer() where TData : unmanaged, IEquatable => new DummyUniformBuffer(); + public IShaderStorageBufferObject CreateShaderStorageBufferObject(int uboSize, int ssboSize) where TData : unmanaged, IEquatable + => new DummyShaderStorageBufferObject(ssboSize); + void IRenderer.SetUniform(IUniformWithValue uniform) { } diff --git a/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs b/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs new file mode 100644 index 0000000000..de7773f434 --- /dev/null +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Graphics.Rendering.Dummy +{ + public class DummyShaderStorageBufferObject : IShaderStorageBufferObject + where T : unmanaged, IEquatable + { + public DummyShaderStorageBufferObject(int size) + { + Size = size; + } + + public int Size { get; } + + public T this[int index] + { + get => default; + set { } + } + + public void Dispose() + { + } + } +} diff --git a/osu.Framework/Graphics/Rendering/IRenderer.cs b/osu.Framework/Graphics/Rendering/IRenderer.cs index 1281a088ab..c97e4914ca 100644 --- a/osu.Framework/Graphics/Rendering/IRenderer.cs +++ b/osu.Framework/Graphics/Rendering/IRenderer.cs @@ -416,6 +416,25 @@ Texture CreateTexture(int width, int height, bool manualMipmaps = false, Texture /// The type of data in the buffer. IUniformBuffer CreateUniformBuffer() where TData : unmanaged, IEquatable; + /// + /// Creates a buffer that can be used to store an array of data for use in a . + /// + /// The number of elements this buffer should contain if Shader Storage Buffer Objects are not supported by the platform. + /// A safe value is 16384/{data_size}. The value must match the definition of the UBO implementation in the shader. + /// The number of elements this buffer should contain if Shader Storage Buffer Objects are supported by the platform. + /// May be any value up to {vram_size}/{data_size}. + /// + /// + /// Internally, this buffer may be implemented as either a "Uniform Buffer Object" (UBO) or + /// a "Shader Storage Buffer Object" (SSBO) depending on the capabilities of the platform. + /// UBOs are more broadly supported but cannot hold as much data as SSBOs. + /// Shaders must provide implementations for both types of buffers to properly support this storage. + /// + /// + /// The type of data to be stored in the buffer. + /// An . + IShaderStorageBufferObject CreateShaderStorageBufferObject(int uboSize, int ssboSize) where TData : unmanaged, IEquatable; + /// /// Sets the value of a uniform. /// diff --git a/osu.Framework/Graphics/Rendering/IShaderStorageBufferObject.cs b/osu.Framework/Graphics/Rendering/IShaderStorageBufferObject.cs new file mode 100644 index 0000000000..630ea5f0c8 --- /dev/null +++ b/osu.Framework/Graphics/Rendering/IShaderStorageBufferObject.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Graphics.Rendering +{ + /// + /// A buffer which stores an array of data for use in a shader. + /// + /// The type of data contained. + public interface IShaderStorageBufferObject : IUniformBuffer + where TData : unmanaged, IEquatable + { + /// + /// The size of this buffer. + /// + int Size { get; } + + /// + /// The data contained by this . + /// + TData this[int index] { get; set; } + } +} diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 3ec64e5ede..1498367dc3 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -1076,6 +1076,9 @@ internal IShader GetMipmapShader() /// protected abstract IUniformBuffer CreateUniformBuffer() where TData : unmanaged, IEquatable; + /// + protected abstract IShaderStorageBufferObject CreateShaderStorageBufferObject(int uboSize, int ssboSize) where TData : unmanaged, IEquatable; + /// /// Creates a new . /// @@ -1187,11 +1190,23 @@ IVertexBatch IRenderer.CreateQuadBatch(int size, int maxBuffer private readonly HashSet validUboTypes = new HashSet(); IUniformBuffer IRenderer.CreateUniformBuffer() + { + validateUniformLayout(); + return CreateUniformBuffer(); + } + + IShaderStorageBufferObject IRenderer.CreateShaderStorageBufferObject(int uboSize, int ssboSize) + { + validateUniformLayout(); + return CreateShaderStorageBufferObject(uboSize, ssboSize); + } + + private void validateUniformLayout() { Trace.Assert(ThreadSafety.IsDrawThread); if (validUboTypes.Contains(typeof(TData))) - return CreateUniformBuffer(); + return; if (typeof(TData).StructLayoutAttribute?.Pack != 1) throw new ArgumentException($"{typeof(TData).ReadableName()} requires a packing size of 1."); @@ -1221,7 +1236,7 @@ IUniformBuffer IRenderer.CreateUniformBuffer() throw new ArgumentException($"{typeof(TData).ReadableName()} alignment requires a {finalPadding} to be added at the end."); validUboTypes.Add(typeof(TData)); - return CreateUniformBuffer(); + return; static void checkValidType(FieldInfo field) { diff --git a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs new file mode 100644 index 0000000000..22964e113d --- /dev/null +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -0,0 +1,184 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Rendering +{ + /// + /// A wrapper around providing push/pop semantics for writing + /// an arbitrary amount of data to an unbounded set of shader storage buffer objects. + /// + public class ShaderStorageBufferObjectStack : IDisposable + where TData : unmanaged, IEquatable + { + /// + /// The index of the current item inside . + /// + public int CurrentIndex => Math.Max(0, currentIndex) % bufferSize; + + /// + /// The buffer that contains the current object. + /// + public IShaderStorageBufferObject CurrentBuffer => buffers[Math.Max(0, currentIndex) / bufferSize]; + + private readonly List> buffers = new List>(); + private readonly Stack lastIndices = new Stack(); + + /// + /// A monotonically increasing (during a frame) index at which items are added to the stack. + /// + private int nextAdditionIndex; + + /// + /// The index of the current item, based on the total size of this stack. + /// This is incremented and decremented during a frame through and . + /// + private int currentIndex = -1; + + /// + /// The size of an individual buffer of this stack. + /// + private readonly int bufferSize; + + private readonly IRenderer renderer; + private readonly int uboSize; + private readonly int ssboSize; + + /// + /// Creates a new . + /// + /// The . + /// Must be at least 2. See + /// Must be at least 2. See + public ShaderStorageBufferObjectStack(IRenderer renderer, int uboSize, int ssboSize) + { + if (uboSize < 2) throw new ArgumentOutOfRangeException(nameof(uboSize), "The size of the buffer must be at least 2."); + if (ssboSize < 2) throw new ArgumentOutOfRangeException(nameof(ssboSize), "The size of the buffer must be at least 2."); + + this.renderer = renderer; + this.uboSize = uboSize; + this.ssboSize = ssboSize; + + ensureCapacity(1); + + bufferSize = buffers[0].Size; + } + + /// + /// Pushes a new item to this stack. + /// + /// The item. + /// The index inside the resulting buffer at which the item is located. + public int Push(TData item) + { + lastIndices.Push(currentIndex); + + int currentBufferIndex = currentIndex / bufferSize; + int currentBufferOffset = currentIndex % bufferSize; + int newIndex = nextAdditionIndex++; + int newBufferIndex = newIndex / bufferSize; + int newBufferOffset = newIndex % bufferSize; + + // Ensure that the item can be stored. + ensureCapacity(newBufferIndex + 1); + + // Flush the pipeline if this invocation transitions to a new buffer. + if (newBufferIndex != currentBufferIndex) + { + renderer.FlushCurrentBatch(FlushBatchSource.SetMasking); + + // + // When transitioning to a new buffer, we want to minimise a certain "thrashing" effect that occurs with successive push/pops. + // For example, suppose the sequence: push -> draw -> pop -> push -> draw -> pop -> etc... + // Each push transitions to buffer X+1 and each pop transitions back to buffer X, resulting in many pipeline flushes. + // + // This is a very specific use case that arises when several consumers push items anonymously from one other. + // + // A little hack is employed to alleviate this issue for ONE push-pop sequence: + // When transitioning to a new buffer, we copy the last item from the last buffer into the new buffer, + // and adjust the stack so that we no longer refer to a position inside the last buffer upon a pop. + // + // If the item to be copied would end up at the last index in the new buffer, then we also need to advance the buffer itself, + // otherwise the user's item would be placed in a new buffer anyway and undo this optimisation. + // + // This is a trade-off of space for performance (by reducing flushes). + // + + // If the copy would be placed at the end of the new buffer, advance the buffer. + if (newBufferOffset == bufferSize - 1) + { + nextAdditionIndex++; + newIndex++; + newBufferIndex++; + newBufferOffset = 0; + + ensureCapacity(newBufferIndex + 1); + } + + // Copy the current item from the last buffer into the new buffer. + buffers[newBufferIndex][newBufferOffset] = buffers[currentBufferIndex][currentBufferOffset]; + + // Adjust the stack so the last index points to the index in the new buffer, instead of currentIndex (from the old buffer). + lastIndices.Pop(); + lastIndices.Push(newIndex); + + nextAdditionIndex++; + newIndex++; + newBufferOffset++; + } + + // Add the item. + buffers[newBufferIndex][newBufferOffset] = item; + currentIndex = newIndex; + + return newBufferOffset; + } + + /// + /// Pops the last item from the stack. + /// + /// + /// This does not remove the item from the stack or the underlying buffer, + /// but adjusts and . + /// + public void Pop() + { + if (currentIndex == -1) + throw new InvalidOperationException("There are no items in the stack to pop."); + + int currentBufferIndex = currentIndex / bufferSize; + int newIndex = lastIndices.Pop(); + int newBufferIndex = newIndex / bufferSize; + + // Flush the pipeline if this invocation transitions to a new buffer. + if (newBufferIndex != currentBufferIndex) + renderer.FlushCurrentBatch(FlushBatchSource.SetMasking); + + currentIndex = newIndex; + } + + /// + /// Clears the stack. + /// + public void Clear() + { + nextAdditionIndex = 0; + currentIndex = -1; + lastIndices.Clear(); + } + + private void ensureCapacity(int size) + { + while (buffers.Count < size) + buffers.Add(renderer.CreateShaderStorageBufferObject(uboSize, ssboSize)); + } + + public void Dispose() + { + foreach (var buffer in buffers) + buffer.Dispose(); + } + } +} diff --git a/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs b/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs new file mode 100644 index 0000000000..4cfc4bd759 --- /dev/null +++ b/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Rendering; +using Veldrid; + +namespace osu.Framework.Graphics.Veldrid.Buffers +{ + internal class VeldridShaderStorageBufferObject : IShaderStorageBufferObject, IVeldridUniformBuffer + where TData : unmanaged, IEquatable + { + public int Size { get; } + + private readonly TData[] data; + private readonly DeviceBuffer buffer; + private readonly VeldridRenderer renderer; + private readonly uint elementSize; + + public VeldridShaderStorageBufferObject(VeldridRenderer renderer, int uboSize, int ssboSize) + { + this.renderer = renderer; + + elementSize = (uint)Marshal.SizeOf(default(TData)); + + if (renderer.UseStructuredBuffers) + { + Size = ssboSize; + buffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(elementSize * Size), BufferUsage.StructuredBufferReadOnly | BufferUsage.Dynamic, elementSize)); + } + else + { + Size = uboSize; + buffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(elementSize * Size), BufferUsage.UniformBuffer | BufferUsage.Dynamic)); + } + + data = new TData[Size]; + } + + private int changeBeginIndex = -1; + private int changeCount; + + public TData this[int index] + { + get => data[index]; + set + { + if (data[index].Equals(value)) + return; + + data[index] = value; + + if (changeBeginIndex == -1) + { + // If this is the first upload, nothing more needs to be done. + changeBeginIndex = index; + } + else + { + // If this is not the first upload, then we need to check if this index is contiguous with the previous changes. + if (index != changeBeginIndex + changeCount) + { + // This index is not contiguous. Flush the current uploads and start a new change set. + flushChanges(); + changeBeginIndex = index; + } + } + + changeCount++; + } + } + + private void flushChanges() + { + if (changeBeginIndex == -1) + return; + + renderer.BufferUpdateCommands.UpdateBuffer(buffer, (uint)(changeBeginIndex * elementSize), data.AsSpan().Slice(changeBeginIndex, changeCount)); + + changeBeginIndex = -1; + changeCount = 0; + } + + public ResourceSet GetResourceSet(ResourceLayout layout) + { + flushChanges(); + return renderer.Factory.CreateResourceSet(new ResourceSetDescription(layout, buffer)); + } + + public void ResetCounters() + { + } + + public void Dispose() + { + buffer.Dispose(); + } + } +} diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index 33662cc107..a0ed9c7306 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -56,6 +56,8 @@ protected internal override bool AllowTearing public override bool IsUvOriginTopLeft => Device.IsUvOriginTopLeft; public override bool IsClipSpaceYInverted => Device.IsClipSpaceYInverted; + public bool UseStructuredBuffers => !FrameworkEnvironment.NoStructuredBuffers && Device.Features.StructuredBuffer; + /// /// Represents the of the latest frame that has completed rendering by the GPU. /// @@ -751,6 +753,9 @@ protected override IVertexBatch CreateQuadBatch(int size, int protected override IUniformBuffer CreateUniformBuffer() => new VeldridUniformBuffer(this); + protected override IShaderStorageBufferObject CreateShaderStorageBufferObject(int uboSize, int ssboSize) + => new VeldridShaderStorageBufferObject(this, uboSize, ssboSize); + protected override INativeTexture CreateNativeTexture(int width, int height, bool manualMipmaps = false, TextureFilteringMode filteringMode = TextureFilteringMode.Linear, Color4? initialisationColour = null) => new VeldridTexture(this, width, height, manualMipmaps, filteringMode.ToSamplerFilter(), initialisationColour); From 3b1bf4b0deaf9db661858a5f6e80a6bfb7c7d2b2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Jul 2023 17:06:24 +0900 Subject: [PATCH 02/18] Add unit test --- .../ShaderStorageBufferObjectStackTest.cs | 236 ++++++++++++++++++ .../Dummy/DummyShaderStorageBufferObject.cs | 7 +- 2 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs diff --git a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs new file mode 100644 index 0000000000..bed99ed093 --- /dev/null +++ b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs @@ -0,0 +1,236 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Dummy; + +namespace osu.Framework.Tests.Graphics +{ + public class ShaderStorageBufferObjectStackTest + { + private const int size = 10; + + private ShaderStorageBufferObjectStack stack = null!; + + [SetUp] + public void Setup() + { + stack = new ShaderStorageBufferObjectStack(new DummyRenderer(), 2, size); + } + + [Test] + public void TestBufferMustBeAtLeast2Elements() + { + Assert.Throws(() => _ = new ShaderStorageBufferObjectStack(new DummyRenderer(), 1, 100)); + Assert.Throws(() => _ = new ShaderStorageBufferObjectStack(new DummyRenderer(), 100, 1)); + Assert.DoesNotThrow(() => _ = new ShaderStorageBufferObjectStack(new DummyRenderer(), 2, 100)); + Assert.DoesNotThrow(() => _ = new ShaderStorageBufferObjectStack(new DummyRenderer(), 100, 2)); + } + + [Test] + public void TestInitialState() + { + Assert.That(stack.CurrentIndex, Is.Zero); + Assert.That(stack.CurrentBuffer, Is.Not.Null); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(0)); + } + + [Test] + public void TestPopWithNoItems() + { + Assert.Throws(() => stack.Pop()); + } + + [Test] + public void TestAddInitialItem() + { + var firstBuffer = stack.CurrentBuffer; + + stack.Push(1); + + Assert.That(stack.CurrentIndex, Is.Zero); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(1)); + } + + [Test] + public void TestPushToFillOneBuffer() + { + var firstBuffer = stack.CurrentBuffer; + int expectedIndex = 0; + + for (int i = 0; i < size; i++) + { + stack.Push(i); + Assert.That(stack.CurrentIndex, Is.EqualTo(expectedIndex++)); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + } + } + + [Test] + public void TestPopEntireBuffer() + { + for (int i = 0; i < size; i++) + stack.Push(i); + + var firstBuffer = stack.CurrentBuffer; + + for (int i = size - 1; i >= 0; i--) + { + Assert.That(stack.CurrentIndex, Is.EqualTo(i)); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + stack.Pop(); + } + } + + [Test] + public void TestTransitionToBufferOnPush() + { + for (int i = 0; i < size; i++) + stack.Push(i); + + var firstBuffer = stack.CurrentBuffer; + int copiedItem = stack.CurrentBuffer[stack.CurrentIndex]; + + // Transition to a new buffer... + stack.Push(size); + Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); + + // ... where the "hack" employed by the queue means that after a transition, the new item is added at index 1... + Assert.That(stack.CurrentIndex, Is.EqualTo(1)); + Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); + + // ... and the first item in the new buffer is a copy of the last referenced item before the push. + Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem)); + } + + [Test] + public void TestTransitionToBufferOnPop() + { + for (int i = 0; i < size; i++) + stack.Push(i); + + var firstBuffer = stack.CurrentBuffer; + int copiedItem = stack.CurrentBuffer[stack.CurrentIndex]; + + // Transition to the new buffer. + stack.Push(size); + + // The "hack" employed means that on the first pop, the index moves to the 0th index in the new buffer. + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.Zero); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem)); + + // After a subsequent pop, we transition to the previous buffer and move to the index prior to the copied item. + // We've already seen the copied item in the new buffer with the above pop, so we should not see it again here. + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem - 1)); + + // Popping once again should move the index further backwards. + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 2)); + } + + [Test] + public void TestTransitionToAndFromNewBufferFromMiddle() + { + for (int i = 0; i < size; i++) + stack.Push(i); + + // Move to the middle of the current buffer (it can not take up any new items at this point). + stack.Pop(); + stack.Pop(); + + var firstBuffer = stack.CurrentBuffer; + int copiedItem = stack.CurrentIndex; + + // Transition to the new buffer... + stack.Push(size); + + // ... and as above, we arrive at index 1 in the new buffer. + Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(1)); + Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); + Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem)); + + // Transition to the previous buffer... + stack.Pop(); + stack.Pop(); + + // ... noting that this is the same as the above "normal" pop case, except that item arrived at is in the middle of the previous buffer. + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem - 1)); + + // Popping once again from this state should move further backwards. + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 2)); + } + + [Test] + public void TestMoveToAndFromMiddleOfNewBuffer() + { + for (int i = 0; i < size; i++) + stack.Push(i); + + var lastBuffer = stack.CurrentBuffer; + int copiedItem1 = stack.CurrentBuffer[stack.CurrentIndex]; + + // Transition to the middle of the new buffer. + stack.Push(size); + stack.Push(size + 1); + Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(lastBuffer)); + Assert.That(stack.CurrentIndex, Is.EqualTo(2)); + Assert.That(stack.CurrentBuffer[2], Is.EqualTo(size + 1)); + Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); + Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem1)); + + // Transition to the previous buffer. + stack.Pop(); + stack.Pop(); + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.EqualTo(lastBuffer)); + + // The item that will be copied into the new buffer. + int copiedItem2 = stack.CurrentBuffer[stack.CurrentIndex]; + + // Transition to the new buffer... + stack.Push(size + 2); + Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(lastBuffer)); + + // ... noting that this is the same as the normal case of transitioning to a new buffer, except arriving in the middle of it... + Assert.That(stack.CurrentIndex, Is.EqualTo(4)); + Assert.That(stack.CurrentBuffer[4], Is.EqualTo(size + 2)); + + // ... where this is the copied item as a result of the immediate push... + Assert.That(stack.CurrentBuffer[3], Is.EqualTo(copiedItem2)); + + // ... and these are the same items from the first pushes above. + Assert.That(stack.CurrentBuffer[2], Is.EqualTo(size + 1)); + Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); + Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem1)); + + // Transition to the previous buffer... + stack.Pop(); + stack.Pop(); + Assert.That(stack.CurrentBuffer, Is.EqualTo(lastBuffer)); + + // ... but this one's a little tricky. The entire process up to this point is: + // 1. From index N-1 -> transition to new buffer. + // 2. Transition to old buffer, arrive at index N-2 (N-1 was copied into the new buffer). + // 3. From index N-2 -> transition to new buffer. + // 4. Transition to old buffer, arrive at index N-3 (N-2 was copied into the new buffer). + Assert.That(stack.CurrentIndex, Is.EqualTo(size - 3)); + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(size - 3)); + } + } +} diff --git a/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs b/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs index de7773f434..dc55c792df 100644 --- a/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs @@ -8,17 +8,20 @@ namespace osu.Framework.Graphics.Rendering.Dummy public class DummyShaderStorageBufferObject : IShaderStorageBufferObject where T : unmanaged, IEquatable { + private readonly T[] data; + public DummyShaderStorageBufferObject(int size) { Size = size; + data = new T[size]; } public int Size { get; } public T this[int index] { - get => default; - set { } + get => data[index]; + set => data[index] = value; } public void Dispose() From 3ba6ecbf24565bc49caf8103ad7a19dc519dc623 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jul 2023 21:40:23 +0900 Subject: [PATCH 03/18] Add shader support and visual test showcasing raw SSBO usage --- .../Resources/Shaders/sh_SSBOTest.fs | 35 ++++ .../Resources/Shaders/sh_SSBOTest.vs | 15 ++ .../TestSceneShaderStorageBufferObject.cs | 167 ++++++++++++++++++ .../Graphics/OpenGL/Shaders/GLShader.cs | 7 +- .../Graphics/OpenGL/Shaders/GLShaderPart.cs | 7 +- .../Graphics/Veldrid/Shaders/VeldridShader.cs | 2 +- .../Veldrid/Shaders/VeldridShaderPart.cs | 22 ++- .../Graphics/Veldrid/VeldridRenderer.cs | 2 +- 8 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.fs create mode 100644 osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs create mode 100644 osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs diff --git a/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.fs b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.fs new file mode 100644 index 0000000000..ee7f4c3c0c --- /dev/null +++ b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.fs @@ -0,0 +1,35 @@ +#ifndef SSBO_TEST_FS +#define SSBO_TEST_FS + +#extension GL_ARB_shader_storage_buffer_object : enable + +struct ColourData +{ + vec4 Colour; +}; + +#ifndef OSU_GRAPHICS_NO_SSBO + +layout(std140, set = 0, binding = 0) readonly buffer g_ColourBuffer +{ + ColourData Data[]; +} ColourBuffer; + +#else // OSU_GRAPHICS_NO_SSBO + +layout(std140, set = 0, binding = 0) uniform g_ColourBuffer +{ + ColourData Data[64]; +} ColourBuffer; + +#endif // OSU_GRAPHICS_NO_SSBO + +layout(location = 0) flat in int v_ColourIndex; +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + o_Colour = ColourBuffer.Data[v_ColourIndex].Colour; +} + +#endif // SSBO_TEST_FS diff --git a/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs new file mode 100644 index 0000000000..7ffc9fca27 --- /dev/null +++ b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs @@ -0,0 +1,15 @@ +#ifndef SSBO_TEST_VS +#define SSBO_TEST_VS + +layout(location = 0) in highp vec2 m_Position; +layout(location = 1) in int m_ColourIndex; + +layout(location = 0) flat out int v_ColourIndex; + +void main(void) +{ + v_ColourIndex = m_ColourIndex; + gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); +} + +#endif \ No newline at end of file diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs new file mode 100644 index 0000000000..78341d8d1d --- /dev/null +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -0,0 +1,167 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osuTK; +using osuTK.Graphics.ES30; + +namespace osu.Framework.Tests.Visual.Graphics +{ + public partial class TestSceneShaderStorageBufferObject : FrameworkTestScene + { + public TestSceneShaderStorageBufferObject() + { + Add(new GridDrawable { RelativeSizeAxes = Axes.Both }); + } + + private partial class GridDrawable : Drawable + { + private const int separation = 1; + private const int size = 32; + + private IShader shader = null!; + + private readonly List areas = new List(); + + [BackgroundDependencyLoader] + private void load(ShaderManager shaderManager) + { + shader = shaderManager.Load("SSBOTest", "SSBOTest"); + } + + protected override void Update() + { + base.Update(); + + areas.Clear(); + + for (float y = 0; y < DrawHeight; y += size + separation) + { + for (float x = 0; x < DrawWidth; x += size + separation) + areas.Add(ToScreenSpace(new RectangleF(x, y, size, size))); + } + + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new GridDrawNode(this); + + private class GridDrawNode : DrawNode + { + private const int min_ssbo_size = 64; + private const int max_ssbo_size = 8192; + + protected new GridDrawable Source => (GridDrawable)base.Source; + + private IShader shader = null!; + private readonly List areas = new List(); + + public GridDrawNode(IDrawable source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + shader = Source.shader; + areas.Clear(); + areas.AddRange(Source.areas); + } + + private IShaderStorageBufferObject? colourBuffer; + private IVertexBatch? vertices; + + public override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + // Create the vertex batch. + vertices ??= renderer.CreateQuadBatch(400, 1000); + + // Create the SSBO. It only needs to be populated once for the demonstration of this test. + if (colourBuffer == null) + { + colourBuffer = renderer.CreateShaderStorageBufferObject(min_ssbo_size, max_ssbo_size); + var rng = new Random(1337); + + for (int i = 0; i < colourBuffer.Size; i++) + colourBuffer[i] = new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }; + } + + // Bind the custom shader and SSBO. + shader.Bind(); + shader.BindUniformBlock("g_ColourBuffer", colourBuffer); + + // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. + for (int i = 0; i < areas.Count; i++) + { + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomLeft, + ColourIndex = i % colourBuffer.Size + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomRight, + ColourIndex = i % colourBuffer.Size + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopRight, + ColourIndex = i % colourBuffer.Size + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopLeft, + ColourIndex = i % colourBuffer.Size + }); + } + + vertices.Draw(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + colourBuffer?.Dispose(); + vertices?.Dispose(); + } + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ColourData + { + public UniformVector4 Colour; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ColourIndexedVertex : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + + [VertexMember(1, VertexAttribPointerType.Int)] + public int ColourIndex; + + public readonly bool Equals(ColourIndexedVertex other) => + Position.Equals(other.Position) + && ColourIndex == other.ColourIndex; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs index 73e6bc29a9..dabe51e00e 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs @@ -191,11 +191,8 @@ private protected virtual bool CompileInternal() Value = textureIndex++ }); } - else if (layout.Elements[0].Kind == ResourceKind.UniformBuffer) - { - var block = new GLUniformBlock(this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++); - uniformBlocks[layout.Elements[0].Name] = block; - } + else + uniformBlocks[layout.Elements[0].Name] = new GLUniformBlock(this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++); } return true; diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs index 2b4cf48cef..4ac5d47819 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs @@ -15,7 +15,7 @@ namespace osu.Framework.Graphics.OpenGL.Shaders internal class GLShaderPart : IShaderPart { public static readonly Regex SHADER_INPUT_PATTERN = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*(in\s+(?:(?:lowp|mediump|highp)\s+)?\w+\s+(\w+)\s*;)", RegexOptions.Multiline); - private static readonly Regex uniform_pattern = new Regex(@"^(\s*layout\s*\(.*)set\s*=\s*(-?\d)(.*\)\s*uniform)", RegexOptions.Multiline); + private static readonly Regex uniform_pattern = new Regex(@"^(\s*layout\s*\(.*)set\s*=\s*(-?\d)(.*\)\s*(?:(?:readonly\s*)?buffer|uniform))", RegexOptions.Multiline); private static readonly Regex include_pattern = new Regex(@"^\s*#\s*include\s+[""<](.*)["">]"); internal bool Compiled { get; private set; } @@ -29,7 +29,7 @@ internal class GLShaderPart : IShaderPart private int partID = -1; - public GLShaderPart(IRenderer renderer, string name, byte[]? data, ShaderType type, IShaderStore store) + public GLShaderPart(GLRenderer renderer, string name, byte[]? data, ShaderType type, IShaderStore store) { this.renderer = renderer; this.store = store; @@ -37,6 +37,9 @@ public GLShaderPart(IRenderer renderer, string name, byte[]? data, ShaderType ty Name = name; Type = type; + if (!renderer.UseStructuredBuffers) + shaderCodes.Add("#define OSU_GRAPHICS_NO_SSBO\n"); + // Load the shader files. shaderCodes.Add(loadFile(data, true)); diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs index a62747a3d7..261446120c 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs @@ -188,7 +188,7 @@ private void compile() textureLayouts.Add(new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout))); } - else if (layout.Elements[0].Kind == ResourceKind.UniformBuffer) + else uniformLayouts[layout.Elements[0].Name] = new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout)); } diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs index 3bd8da9c72..c776118447 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs @@ -17,28 +17,34 @@ namespace osu.Framework.Graphics.Veldrid.Shaders internal class VeldridShaderPart : IShaderPart { private static readonly Regex shader_input_pattern = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*in\s+((?:(?:lowp|mediump|highp)\s+)?\w+)\s+(\w+)\s*;", RegexOptions.Multiline); - private static readonly Regex shader_output_pattern = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*out\s+((?:(?:lowp|mediump|highp)\s+)?\w+)\s+(\w+)\s*;", RegexOptions.Multiline); - private static readonly Regex uniform_pattern = new Regex(@"^(\s*layout\s*\(.*)set\s*=\s*(-?\d)(.*\)\s*uniform)", RegexOptions.Multiline); + + private static readonly Regex shader_output_pattern = + new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*out\s+((?:(?:lowp|mediump|highp)\s+)?\w+)\s+(\w+)\s*;", RegexOptions.Multiline); + + private static readonly Regex uniform_pattern = new Regex(@"^(\s*layout\s*\(.*)set\s*=\s*(-?\d)(.*\)\s*(?:(?:readonly\s*)?buffer|uniform))", RegexOptions.Multiline); private static readonly Regex include_pattern = new Regex(@"^\s*#\s*include\s+[""<](.*)["">]"); public readonly ShaderPartType Type; private string header = string.Empty; - private readonly string code; + private readonly string code = string.Empty; private readonly IShaderStore store; public readonly List Inputs = new List(); public readonly List Outputs = new List(); - public VeldridShaderPart(byte[]? data, ShaderPartType type, IShaderStore store) + public VeldridShaderPart(VeldridRenderer renderer, byte[]? data, ShaderPartType type, IShaderStore store) { this.store = store; Type = type; + if (!renderer.UseStructuredBuffers) + code = "#define OSU_GRAPHICS_NO_SSBO\n"; + // Load the shader files. - code = loadFile(data, true); + code += loadFile(data, true); int lastInputIndex = 0; @@ -114,8 +120,10 @@ private string loadFile(byte[]? bytes, bool mainFile) internalIncludes += loadFile(store.GetRawData("Internal/sh_GlobalUniforms.h"), false) + "\n"; result = internalIncludes + result; - Inputs.AddRange(shader_input_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList()); - Outputs.AddRange(shader_output_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList()); + Inputs.AddRange( + shader_input_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList()); + Outputs.AddRange(shader_output_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)) + .ToList()); string outputCode = loadFile(store.GetRawData($"Internal/sh_{Type}_Output.h"), false); diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index a0ed9c7306..c21a6c5123 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -730,7 +730,7 @@ private bool waitForFence(Fence fence, int millisecondsTimeout) } protected override IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType) - => new VeldridShaderPart(rawData, partType, store); + => new VeldridShaderPart(this, rawData, partType, store); protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) => new VeldridShader(this, name, parts.Cast().ToArray(), globalUniformBuffer, compilationStore); From ee350888d4cac49153394b96a35c0a2d88b9532c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 01:39:45 +0900 Subject: [PATCH 04/18] Remove unused buffer binding method --- osu.Framework/Graphics/OpenGL/GLRenderer.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 1c95814649..052f5a1af0 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -58,8 +58,6 @@ protected internal override bool VerticalSync private int backbufferFramebuffer; - private readonly int[] lastBoundBuffers = new int[2]; - private bool? lastBlendingEnabledState; private int lastBoundVertexArray; @@ -113,7 +111,6 @@ protected virtual string GetExtensions() protected internal override void BeginFrame(Vector2 windowSize) { lastBlendingEnabledState = null; - lastBoundBuffers.AsSpan().Clear(); lastBoundVertexArray = 0; // Seems to be required on some drivers as the context is lost from the draw thread. @@ -145,19 +142,6 @@ public bool BindVertexArray(int vaoId) return true; } - public bool BindBuffer(BufferTarget target, int buffer) - { - int bufferIndex = target - BufferTarget.ArrayBuffer; - if (lastBoundBuffers[bufferIndex] == buffer) - return false; - - lastBoundBuffers[bufferIndex] = buffer; - GL.BindBuffer(target, buffer); - - FrameStatistics.Increment(StatisticsCounterType.VBufBinds); - return true; - } - protected override void SetShaderImplementation(IShader shader) => GL.UseProgram((GLShader)shader); protected override void SetUniformImplementation(IUniformWithValue uniform) From e11a4fc9d7dff16842fce28fdb8940eea1a332b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 02:01:49 +0900 Subject: [PATCH 05/18] Refactor/fix GL ubo/ssbo binding Now looks and behaves much more like the Veldrid implementation. The extra call to `glUniformBlockBinding` is unlikely to be an issue - although this is the main drawing function, it's not "hot" per-se (in that it shouldn't receive more than 1000 draw calls in an extreme setting). SSBOs and UBOs don't share the same binding points - `g_GlobalUniforms` and `g_ColourBuffer` (in the test scene's example) could (and in practice, do) have an index of 0. What discerns them is the range type, which is why we need to use `glUniformBlockBinding` for one and `glShaderStorageBlockBinding` for the other. --- .../OpenGL/Buffers/GLUniformBuffer.cs | 9 +++-- .../OpenGL/Buffers/IGLUniformBuffer.cs | 12 +++++++ osu.Framework/Graphics/OpenGL/GLRenderer.cs | 34 +++++++++++++++++++ .../Graphics/OpenGL/Shaders/GLShader.cs | 24 ++++++++++--- 4 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 osu.Framework/Graphics/OpenGL/Buffers/IGLUniformBuffer.cs diff --git a/osu.Framework/Graphics/OpenGL/Buffers/GLUniformBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/GLUniformBuffer.cs index 45f208e0eb..fe019075ed 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/GLUniformBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/GLUniformBuffer.cs @@ -8,11 +8,6 @@ namespace osu.Framework.Graphics.OpenGL.Buffers { - internal interface IGLUniformBuffer - { - int Id { get; } - } - internal class GLUniformBuffer : IUniformBuffer, IGLUniformBuffer where TData : unmanaged, IEquatable { @@ -80,5 +75,9 @@ protected virtual void Dispose(bool disposing) #endregion public int Id => uboId; + + public void Flush() + { + } } } diff --git a/osu.Framework/Graphics/OpenGL/Buffers/IGLUniformBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/IGLUniformBuffer.cs new file mode 100644 index 0000000000..5c2dcf3dc0 --- /dev/null +++ b/osu.Framework/Graphics/OpenGL/Buffers/IGLUniformBuffer.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + internal interface IGLUniformBuffer + { + int Id { get; } + + void Flush(); + } +} diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 052f5a1af0..f134e65b97 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.InteropServices; @@ -25,6 +26,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Image = SixLabors.ImageSharp.Image; +using GL4 = osuTK.Graphics.OpenGL; namespace osu.Framework.Graphics.OpenGL { @@ -58,6 +60,7 @@ protected internal override bool VerticalSync private int backbufferFramebuffer; + private readonly Dictionary boundUniformBuffers = new Dictionary(); private bool? lastBlendingEnabledState; private int lastBoundVertexArray; @@ -112,6 +115,7 @@ protected internal override void BeginFrame(Vector2 windowSize) { lastBlendingEnabledState = null; lastBoundVertexArray = 0; + boundUniformBuffers.Clear(); // Seems to be required on some drivers as the context is lost from the draw thread. MakeCurrent(); @@ -263,12 +267,42 @@ protected override void ClearImplementation(ClearInfo clearInfo) GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); } + public void BindUniformBuffer(string blockName, IGLUniformBuffer glBuffer) + { + FlushCurrentBatch(FlushBatchSource.BindBuffer); + boundUniformBuffers[blockName] = glBuffer; + } + public void DrawVertices(PrimitiveType type, int vertexStart, int verticesCount) { var glShader = (GLShader)Shader!; glShader.BindUniformBlock("g_GlobalUniforms", GlobalUniformBuffer!); + int currentUniformBinding = 0; + int currentStorageBinding = 0; + + foreach ((string name, IGLUniformBuffer buffer) in boundUniformBuffers) + { + if (glShader.GetUniformBlockIndex(name) is not int index) + continue; + + buffer.Flush(); + + if (buffer is IGLShaderStorageBufferObject && UseStructuredBuffers) + { + GL4.GL.ShaderStorageBlockBinding(glShader, index, currentStorageBinding); + GL4.GL.BindBufferBase(GL4.BufferRangeTarget.ShaderStorageBuffer, currentStorageBinding, buffer.Id); + currentStorageBinding++; + } + else + { + GL.UniformBlockBinding(glShader, index, currentUniformBinding); + GL.BindBufferBase(BufferRangeTarget.UniformBuffer, currentUniformBinding, buffer.Id); + currentUniformBinding++; + } + } + GL.DrawElements(type, verticesCount, DrawElementsType.UnsignedShort, (IntPtr)(vertexStart * sizeof(ushort))); } diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs index 1622955592..b94033a466 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs @@ -13,6 +13,8 @@ using Veldrid; using Veldrid.SPIRV; using static osu.Framework.Threading.ScheduledDelegate; +using GL4 = osuTK.Graphics.OpenGL; +using ProgramInterface = osuTK.Graphics.OpenGL.ProgramInterface; namespace osu.Framework.Graphics.OpenGL.Shaders { @@ -28,7 +30,7 @@ internal class GLShader : IShader IReadOnlyDictionary IShader.Uniforms => Uniforms; - private readonly Dictionary uniformBlocks = new Dictionary(); + private readonly Dictionary uniformBlocks = new Dictionary(); private readonly List> textureUniforms = new List>(); public bool IsLoaded { get; private set; } @@ -136,6 +138,8 @@ public Uniform GetUniform(string name) return (Uniform)Uniforms[name]; } + public int? GetUniformBlockIndex(string name) => uniformBlocks.TryGetValue(name, out int index) ? index : null; + public virtual void BindUniformBlock(string blockName, IUniformBuffer buffer) { if (buffer is not IGLUniformBuffer glBuffer) @@ -146,8 +150,7 @@ public virtual void BindUniformBlock(string blockName, IUniformBuffer buffer) EnsureShaderCompiled(); - renderer.FlushCurrentBatch(FlushBatchSource.BindBuffer); - GL.BindBufferBase(BufferRangeTarget.UniformBuffer, uniformBlocks[blockName].Binding, glBuffer.Id); + renderer.BindUniformBuffer(blockName, glBuffer); } private protected virtual bool CompileInternal() @@ -167,7 +170,6 @@ private protected virtual bool CompileInternal() if (linkResult != 1) return false; - int blockBindingIndex = 0; int textureIndex = 0; foreach (ResourceLayoutDescription layout in compilation.Reflection.ResourceLayouts) @@ -188,7 +190,19 @@ private protected virtual bool CompileInternal() }); } else - uniformBlocks[layout.Elements[0].Name] = new GLUniformBlock(this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++); + { + switch (layout.Elements[0].Kind) + { + case ResourceKind.UniformBuffer: + uniformBlocks[layout.Elements[0].Name] = GL.GetUniformBlockIndex(this, layout.Elements[0].Name); + break; + + case ResourceKind.StructuredBufferReadOnly: + case ResourceKind.StructuredBufferReadWrite: + uniformBlocks[layout.Elements[0].Name] = GL4.GL.GetProgramResourceIndex(this, ProgramInterface.ShaderStorageBlock, layout.Elements[0].Name); + break; + } + } } return true; From 5e9276dfc558c6666dcad056741059f44fbd3365 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 03:17:23 +0900 Subject: [PATCH 06/18] Add implementation demonstrating stack-based usage --- .../TestSceneShaderStorageBufferObject.cs | 250 +++++++++++++----- 1 file changed, 179 insertions(+), 71 deletions(-) diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 78341d8d1d..ce7ee456c8 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; @@ -18,9 +19,26 @@ namespace osu.Framework.Tests.Visual.Graphics { public partial class TestSceneShaderStorageBufferObject : FrameworkTestScene { - public TestSceneShaderStorageBufferObject() + private const int ubo_size = 64; + private const int ssbo_size = 8192; + + [Test] + public void TestRawStorageBuffer() + { + AddStep("add grid", () => Child = new GridDrawable + { + RelativeSizeAxes = Axes.Both, + RawBuffer = true + }); + } + + [Test] + public void TestStorageBufferStack() { - Add(new GridDrawable { RelativeSizeAxes = Axes.Both }); + AddStep("add grid", () => Child = new GridDrawable + { + RelativeSizeAxes = Axes.Both + }); } private partial class GridDrawable : Drawable @@ -28,119 +46,209 @@ private partial class GridDrawable : Drawable private const int separation = 1; private const int size = 32; - private IShader shader = null!; + public bool RawBuffer; - private readonly List areas = new List(); + public IShader Shader { get; private set; } = null!; + public List Areas { get; } = new List(); [BackgroundDependencyLoader] private void load(ShaderManager shaderManager) { - shader = shaderManager.Load("SSBOTest", "SSBOTest"); + Shader = shaderManager.Load("SSBOTest", "SSBOTest"); } protected override void Update() { base.Update(); - areas.Clear(); + Areas.Clear(); for (float y = 0; y < DrawHeight; y += size + separation) { for (float x = 0; x < DrawWidth; x += size + separation) - areas.Add(ToScreenSpace(new RectangleF(x, y, size, size))); + Areas.Add(ToScreenSpace(new RectangleF(x, y, size, size))); } Invalidate(Invalidation.DrawNode); } - protected override DrawNode CreateDrawNode() => new GridDrawNode(this); + protected override DrawNode CreateDrawNode() => RawBuffer ? new RawStorageBufferDrawNode(this) : new StorageBufferStackDrawNode(this); + } + + /// + /// This implementation demonstrates the usage of a raw . + /// + private class RawStorageBufferDrawNode : DrawNode + { + protected new GridDrawable Source => (GridDrawable)base.Source; + + private IShader shader = null!; + private readonly List areas = new List(); + + public RawStorageBufferDrawNode(IDrawable source) + : base(source) + { + } - private class GridDrawNode : DrawNode + public override void ApplyState() { - private const int min_ssbo_size = 64; - private const int max_ssbo_size = 8192; + base.ApplyState(); + + shader = Source.Shader; + areas.Clear(); + areas.AddRange(Source.Areas); + } + + private IShaderStorageBufferObject? colourBuffer; + private IVertexBatch? vertices; - protected new GridDrawable Source => (GridDrawable)base.Source; + public override void Draw(IRenderer renderer) + { + base.Draw(renderer); - private IShader shader = null!; - private readonly List areas = new List(); + // Create the vertex batch. + vertices ??= renderer.CreateQuadBatch(400, 1000); - public GridDrawNode(IDrawable source) - : base(source) + // Create the SSBO. It only needs to be populated once for the demonstration of this test. + if (colourBuffer == null) { + colourBuffer = renderer.CreateShaderStorageBufferObject(ubo_size, ssbo_size); + var rng = new Random(1337); + + for (int i = 0; i < colourBuffer.Size; i++) + colourBuffer[i] = new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }; } - public override void ApplyState() + // Bind the custom shader and SSBO. + shader.Bind(); + shader.BindUniformBlock("g_ColourBuffer", colourBuffer); + + // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. + for (int i = 0; i < areas.Count; i++) { - base.ApplyState(); + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomLeft, + ColourIndex = i % colourBuffer.Size + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomRight, + ColourIndex = i % colourBuffer.Size + }); - shader = Source.shader; - areas.Clear(); - areas.AddRange(Source.areas); + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopRight, + ColourIndex = i % colourBuffer.Size + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopLeft, + ColourIndex = i % colourBuffer.Size + }); } - private IShaderStorageBufferObject? colourBuffer; - private IVertexBatch? vertices; + vertices.Draw(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + colourBuffer?.Dispose(); + vertices?.Dispose(); + } + } + + /// + /// This implementation demonstrates the usage of a . + /// Note that unlike the above implementation, this one provides more randomness when run with OSU_GRAPHICS_NO_SSBO=1, + /// due to the unlimited size of . + /// + private class StorageBufferStackDrawNode : DrawNode + { + protected new GridDrawable Source => (GridDrawable)base.Source; + + private IShader shader = null!; + private readonly List areas = new List(); + + public StorageBufferStackDrawNode(IDrawable source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + shader = Source.Shader; + areas.Clear(); + areas.AddRange(Source.Areas); + } + + private ShaderStorageBufferObjectStack? colourBuffer; + private IVertexBatch? vertices; + + public override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + // Create the vertex batch. + vertices ??= renderer.CreateQuadBatch(400, 1000); + + // Create the SSBO. + colourBuffer ??= new ShaderStorageBufferObjectStack(renderer, ubo_size, ssbo_size); + + var rng = new Random(1337); - public override void Draw(IRenderer renderer) + // Bind the custom shader. + shader.Bind(); + + // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. + for (int i = 0; i < areas.Count; i++) { - base.Draw(renderer); + int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); - // Create the vertex batch. - vertices ??= renderer.CreateQuadBatch(400, 1000); + // Bind the SSBO. This may change between invocations if the buffer overflows in the above push. + shader.BindUniformBlock("g_ColourBuffer", colourBuffer.CurrentBuffer); - // Create the SSBO. It only needs to be populated once for the demonstration of this test. - if (colourBuffer == null) + vertices.Add(new ColourIndexedVertex { - colourBuffer = renderer.CreateShaderStorageBufferObject(min_ssbo_size, max_ssbo_size); - var rng = new Random(1337); + Position = areas[i].BottomLeft, + ColourIndex = colourIndex + }); - for (int i = 0; i < colourBuffer.Size; i++) - colourBuffer[i] = new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }; - } + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomRight, + ColourIndex = colourIndex + }); - // Bind the custom shader and SSBO. - shader.Bind(); - shader.BindUniformBlock("g_ColourBuffer", colourBuffer); + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopRight, + ColourIndex = colourIndex + }); - // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. - for (int i = 0; i < areas.Count; i++) + vertices.Add(new ColourIndexedVertex { - vertices.Add(new ColourIndexedVertex - { - Position = areas[i].BottomLeft, - ColourIndex = i % colourBuffer.Size - }); - - vertices.Add(new ColourIndexedVertex - { - Position = areas[i].BottomRight, - ColourIndex = i % colourBuffer.Size - }); - - vertices.Add(new ColourIndexedVertex - { - Position = areas[i].TopRight, - ColourIndex = i % colourBuffer.Size - }); - - vertices.Add(new ColourIndexedVertex - { - Position = areas[i].TopLeft, - ColourIndex = i % colourBuffer.Size - }); - } - - vertices.Draw(); + Position = areas[i].TopLeft, + ColourIndex = colourIndex + }); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + vertices.Draw(); + } - colourBuffer?.Dispose(); - vertices?.Dispose(); - } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + colourBuffer?.Dispose(); + vertices?.Dispose(); } } From 43a898e02b804c15f1e52a7f3f49974bf067b023 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 03:17:36 +0900 Subject: [PATCH 07/18] Prevent pipeline flushes when rebinding UBOs --- osu.Framework/Graphics/OpenGL/GLRenderer.cs | 3 +++ osu.Framework/Graphics/Veldrid/VeldridRenderer.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index f134e65b97..01bd443875 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -269,6 +269,9 @@ protected override void ClearImplementation(ClearInfo clearInfo) public void BindUniformBuffer(string blockName, IGLUniformBuffer glBuffer) { + if (boundUniformBuffers.TryGetValue(blockName, out IGLUniformBuffer? current) && current == glBuffer) + return; + FlushCurrentBatch(FlushBatchSource.BindBuffer); boundUniformBuffers[blockName] = glBuffer; } diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index 47b815c3ce..fae3888541 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -526,6 +526,9 @@ public void BindIndexBuffer(VeldridIndexLayout layout, int verticesCount) public void BindUniformBuffer(string blockName, IVeldridUniformBuffer veldridBuffer) { + if (boundUniformBuffers.TryGetValue(blockName, out IVeldridUniformBuffer? current) && current == veldridBuffer) + return; + FlushCurrentBatch(FlushBatchSource.BindBuffer); boundUniformBuffers[blockName] = veldridBuffer; } From f3bff983b1e67da3237a288554fb24d69d985c13 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 03:19:46 +0900 Subject: [PATCH 08/18] Convert indentation to spaces --- osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs index 7ffc9fca27..23c4835300 100644 --- a/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs +++ b/osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs @@ -8,8 +8,8 @@ layout(location = 0) flat out int v_ColourIndex; void main(void) { - v_ColourIndex = m_ColourIndex; - gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); + v_ColourIndex = m_ColourIndex; + gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); } #endif \ No newline at end of file From 5ea8e8219f51c85dcf7ddd1a3669eb118a7ae0d8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 03:35:16 +0900 Subject: [PATCH 09/18] Fix failing edge case when transitioning from empty stack --- .../ShaderStorageBufferObjectStackTest.cs | 22 +++++++++++++++++++ .../TestSceneShaderStorageBufferObject.cs | 5 ++++- .../ShaderStorageBufferObjectStack.cs | 17 +++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs index bed99ed093..1480951762 100644 --- a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs +++ b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs @@ -232,5 +232,27 @@ public void TestMoveToAndFromMiddleOfNewBuffer() Assert.That(stack.CurrentIndex, Is.EqualTo(size - 3)); Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(size - 3)); } + + [Test] + public void TestTransitionFromEmptyStack() + { + for (int i = 0; i < size * 2; i++) + { + var lastBuffer = stack.CurrentBuffer; + + // Push one item. + stack.Push(i); + + // On a buffer transition, test that the item at the 0-th index of the first buffer was correct copied to the new buffer. + if (stack.CurrentBuffer != lastBuffer) + Assert.That(stack.CurrentBuffer[stack.CurrentIndex - 1], Is.EqualTo(0)); + + // Test that the item was correctly placed in the new buffer + Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + + // Return to an empty stack. + stack.Pop(); + } + } } } diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index ce7ee456c8..b7635e2d31 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -212,7 +212,7 @@ public override void Draw(IRenderer renderer) { int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); - // Bind the SSBO. This may change between invocations if the buffer overflows in the above push. + // Bind the SSBO. This may change between iterations if a buffer transition happens via the above push. shader.BindUniformBlock("g_ColourBuffer", colourBuffer.CurrentBuffer); vertices.Add(new ColourIndexedVertex @@ -238,6 +238,9 @@ public override void Draw(IRenderer renderer) Position = areas[i].TopLeft, ColourIndex = colourIndex }); + + // This isn't really required when using ShaderStorageBufferObjectStack in this isolated form, but is good practice. + colourBuffer.Pop(); } vertices.Draw(); diff --git a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs index 22964e113d..15c0b0037a 100644 --- a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -16,12 +16,22 @@ public class ShaderStorageBufferObjectStack : IDisposable /// /// The index of the current item inside . /// - public int CurrentIndex => Math.Max(0, currentIndex) % bufferSize; + public int CurrentIndex => currentBufferOffset; /// /// The buffer that contains the current object. /// - public IShaderStorageBufferObject CurrentBuffer => buffers[Math.Max(0, currentIndex) / bufferSize]; + public IShaderStorageBufferObject CurrentBuffer => buffers[currentBufferIndex]; + + /// + /// The index of the item inside the buffer containing it. + /// + private int currentBufferOffset => currentIndex == -1 ? 0 : currentIndex % bufferSize; + + /// + /// The index of the buffer containing the current item. + /// + private int currentBufferIndex => currentIndex == -1 ? 0 : currentIndex / bufferSize; private readonly List> buffers = new List>(); private readonly Stack lastIndices = new Stack(); @@ -75,8 +85,6 @@ public int Push(TData item) { lastIndices.Push(currentIndex); - int currentBufferIndex = currentIndex / bufferSize; - int currentBufferOffset = currentIndex % bufferSize; int newIndex = nextAdditionIndex++; int newBufferIndex = newIndex / bufferSize; int newBufferOffset = newIndex % bufferSize; @@ -148,7 +156,6 @@ public void Pop() if (currentIndex == -1) throw new InvalidOperationException("There are no items in the stack to pop."); - int currentBufferIndex = currentIndex / bufferSize; int newIndex = lastIndices.Pop(); int newBufferIndex = newIndex / bufferSize; From 5648bd781921fce43d4b833dad415289a65b78e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 03:35:44 +0900 Subject: [PATCH 10/18] Rename CurrentIndex -> CurrentOffset --- .../ShaderStorageBufferObjectStackTest.cs | 58 +++++++++---------- .../ShaderStorageBufferObjectStack.cs | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs index 1480951762..a8561e3cc7 100644 --- a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs +++ b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs @@ -32,9 +32,9 @@ public void TestBufferMustBeAtLeast2Elements() [Test] public void TestInitialState() { - Assert.That(stack.CurrentIndex, Is.Zero); + Assert.That(stack.CurrentOffset, Is.Zero); Assert.That(stack.CurrentBuffer, Is.Not.Null); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(0)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(0)); } [Test] @@ -50,9 +50,9 @@ public void TestAddInitialItem() stack.Push(1); - Assert.That(stack.CurrentIndex, Is.Zero); + Assert.That(stack.CurrentOffset, Is.Zero); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(1)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(1)); } [Test] @@ -64,9 +64,9 @@ public void TestPushToFillOneBuffer() for (int i = 0; i < size; i++) { stack.Push(i); - Assert.That(stack.CurrentIndex, Is.EqualTo(expectedIndex++)); + Assert.That(stack.CurrentOffset, Is.EqualTo(expectedIndex++)); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(i)); } } @@ -80,9 +80,9 @@ public void TestPopEntireBuffer() for (int i = size - 1; i >= 0; i--) { - Assert.That(stack.CurrentIndex, Is.EqualTo(i)); + Assert.That(stack.CurrentOffset, Is.EqualTo(i)); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(i)); stack.Pop(); } } @@ -94,14 +94,14 @@ public void TestTransitionToBufferOnPush() stack.Push(i); var firstBuffer = stack.CurrentBuffer; - int copiedItem = stack.CurrentBuffer[stack.CurrentIndex]; + int copiedItem = stack.CurrentBuffer[stack.CurrentOffset]; // Transition to a new buffer... stack.Push(size); Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); // ... where the "hack" employed by the queue means that after a transition, the new item is added at index 1... - Assert.That(stack.CurrentIndex, Is.EqualTo(1)); + Assert.That(stack.CurrentOffset, Is.EqualTo(1)); Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); // ... and the first item in the new buffer is a copy of the last referenced item before the push. @@ -115,7 +115,7 @@ public void TestTransitionToBufferOnPop() stack.Push(i); var firstBuffer = stack.CurrentBuffer; - int copiedItem = stack.CurrentBuffer[stack.CurrentIndex]; + int copiedItem = stack.CurrentBuffer[stack.CurrentOffset]; // Transition to the new buffer. stack.Push(size); @@ -123,20 +123,20 @@ public void TestTransitionToBufferOnPop() // The "hack" employed means that on the first pop, the index moves to the 0th index in the new buffer. stack.Pop(); Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.Zero); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem)); + Assert.That(stack.CurrentOffset, Is.Zero); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(copiedItem)); // After a subsequent pop, we transition to the previous buffer and move to the index prior to the copied item. // We've already seen the copied item in the new buffer with the above pop, so we should not see it again here. stack.Pop(); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 1)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentOffset, Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(copiedItem - 1)); // Popping once again should move the index further backwards. stack.Pop(); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 2)); + Assert.That(stack.CurrentOffset, Is.EqualTo(copiedItem - 2)); } [Test] @@ -150,14 +150,14 @@ public void TestTransitionToAndFromNewBufferFromMiddle() stack.Pop(); var firstBuffer = stack.CurrentBuffer; - int copiedItem = stack.CurrentIndex; + int copiedItem = stack.CurrentOffset; // Transition to the new buffer... stack.Push(size); // ... and as above, we arrive at index 1 in the new buffer. Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(1)); + Assert.That(stack.CurrentOffset, Is.EqualTo(1)); Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem)); @@ -167,13 +167,13 @@ public void TestTransitionToAndFromNewBufferFromMiddle() // ... noting that this is the same as the above "normal" pop case, except that item arrived at is in the middle of the previous buffer. Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 1)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentOffset, Is.EqualTo(copiedItem - 1)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(copiedItem - 1)); // Popping once again from this state should move further backwards. stack.Pop(); Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(copiedItem - 2)); + Assert.That(stack.CurrentOffset, Is.EqualTo(copiedItem - 2)); } [Test] @@ -183,13 +183,13 @@ public void TestMoveToAndFromMiddleOfNewBuffer() stack.Push(i); var lastBuffer = stack.CurrentBuffer; - int copiedItem1 = stack.CurrentBuffer[stack.CurrentIndex]; + int copiedItem1 = stack.CurrentBuffer[stack.CurrentOffset]; // Transition to the middle of the new buffer. stack.Push(size); stack.Push(size + 1); Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(lastBuffer)); - Assert.That(stack.CurrentIndex, Is.EqualTo(2)); + Assert.That(stack.CurrentOffset, Is.EqualTo(2)); Assert.That(stack.CurrentBuffer[2], Is.EqualTo(size + 1)); Assert.That(stack.CurrentBuffer[1], Is.EqualTo(size)); Assert.That(stack.CurrentBuffer[0], Is.EqualTo(copiedItem1)); @@ -201,14 +201,14 @@ public void TestMoveToAndFromMiddleOfNewBuffer() Assert.That(stack.CurrentBuffer, Is.EqualTo(lastBuffer)); // The item that will be copied into the new buffer. - int copiedItem2 = stack.CurrentBuffer[stack.CurrentIndex]; + int copiedItem2 = stack.CurrentBuffer[stack.CurrentOffset]; // Transition to the new buffer... stack.Push(size + 2); Assert.That(stack.CurrentBuffer, Is.Not.EqualTo(lastBuffer)); // ... noting that this is the same as the normal case of transitioning to a new buffer, except arriving in the middle of it... - Assert.That(stack.CurrentIndex, Is.EqualTo(4)); + Assert.That(stack.CurrentOffset, Is.EqualTo(4)); Assert.That(stack.CurrentBuffer[4], Is.EqualTo(size + 2)); // ... where this is the copied item as a result of the immediate push... @@ -229,8 +229,8 @@ public void TestMoveToAndFromMiddleOfNewBuffer() // 2. Transition to old buffer, arrive at index N-2 (N-1 was copied into the new buffer). // 3. From index N-2 -> transition to new buffer. // 4. Transition to old buffer, arrive at index N-3 (N-2 was copied into the new buffer). - Assert.That(stack.CurrentIndex, Is.EqualTo(size - 3)); - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(size - 3)); + Assert.That(stack.CurrentOffset, Is.EqualTo(size - 3)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(size - 3)); } [Test] @@ -245,10 +245,10 @@ public void TestTransitionFromEmptyStack() // On a buffer transition, test that the item at the 0-th index of the first buffer was correct copied to the new buffer. if (stack.CurrentBuffer != lastBuffer) - Assert.That(stack.CurrentBuffer[stack.CurrentIndex - 1], Is.EqualTo(0)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset - 1], Is.EqualTo(0)); // Test that the item was correctly placed in the new buffer - Assert.That(stack.CurrentBuffer[stack.CurrentIndex], Is.EqualTo(i)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(i)); // Return to an empty stack. stack.Pop(); diff --git a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs index 15c0b0037a..bc307336de 100644 --- a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -16,7 +16,7 @@ public class ShaderStorageBufferObjectStack : IDisposable /// /// The index of the current item inside . /// - public int CurrentIndex => currentBufferOffset; + public int CurrentOffset => currentBufferOffset; /// /// The buffer that contains the current object. @@ -149,7 +149,7 @@ public int Push(TData item) /// /// /// This does not remove the item from the stack or the underlying buffer, - /// but adjusts and . + /// but adjusts and . /// public void Pop() { From c39f4c7f34863697f6118edf1229be1eac92955a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 08:59:57 +0900 Subject: [PATCH 11/18] Add new flush batch source for SSBO stack --- osu.Framework/Graphics/Rendering/FlushBatchSource.cs | 3 ++- .../Graphics/Rendering/ShaderStorageBufferObjectStack.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Graphics/Rendering/FlushBatchSource.cs b/osu.Framework/Graphics/Rendering/FlushBatchSource.cs index 73f8729ed6..13e557e544 100644 --- a/osu.Framework/Graphics/Rendering/FlushBatchSource.cs +++ b/osu.Framework/Graphics/Rendering/FlushBatchSource.cs @@ -19,6 +19,7 @@ public enum FlushBatchSource SetStencilInfo, SetUniform, UnbindTexture, - SetActiveBatch + SetActiveBatch, + StorageBufferOverflow } } diff --git a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs index bc307336de..f93e94d872 100644 --- a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -95,7 +95,7 @@ public int Push(TData item) // Flush the pipeline if this invocation transitions to a new buffer. if (newBufferIndex != currentBufferIndex) { - renderer.FlushCurrentBatch(FlushBatchSource.SetMasking); + renderer.FlushCurrentBatch(FlushBatchSource.StorageBufferOverflow); // // When transitioning to a new buffer, we want to minimise a certain "thrashing" effect that occurs with successive push/pops. @@ -161,7 +161,7 @@ public void Pop() // Flush the pipeline if this invocation transitions to a new buffer. if (newBufferIndex != currentBufferIndex) - renderer.FlushCurrentBatch(FlushBatchSource.SetMasking); + renderer.FlushCurrentBatch(FlushBatchSource.StorageBufferOverflow); currentIndex = newIndex; } From 1996ca903629815b75d4748de204fc87225c348c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 29 Jul 2023 09:01:24 +0900 Subject: [PATCH 12/18] Log value of UseStructuredBuffers --- osu.Framework/Graphics/OpenGL/GLRenderer.cs | 2 ++ osu.Framework/Graphics/Veldrid/VeldridRenderer.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 01bd443875..f864712d88 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -94,6 +94,8 @@ protected override void Initialise(IGraphicsSurface graphicsSurface) UseStructuredBuffers = extensions.Contains(@"GL_ARB_shader_storage_buffer_object") && !FrameworkEnvironment.NoStructuredBuffers; + Logger.Log($"{nameof(UseStructuredBuffers)}: {UseStructuredBuffers}"); + openGLSurface.ClearCurrent(); } diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index fae3888541..57ce6ad3ec 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -221,6 +221,8 @@ protected override void Initialise(IGraphicsSurface graphicsSurface) break; } + Logger.Log($"{nameof(UseStructuredBuffers)}: {UseStructuredBuffers}"); + MaxTextureSize = maxTextureSize; Commands = Factory.CreateCommandList(); From 2ca906efc81c0e7d4e69b7b0d2e6f8cfe32dff9a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Aug 2023 12:12:20 +0900 Subject: [PATCH 13/18] Apply refactorings from code review --- .../TestSceneShaderStorageBufferObject.cs | 1 - .../OpenGL/Buffers/GLShaderStorageBufferObject.cs | 15 +++------------ .../Graphics/Veldrid/Shaders/VeldridShader.cs | 11 ++++++++++- .../Graphics/Veldrid/Shaders/VeldridShaderPart.cs | 4 ++-- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index b7635e2d31..3189e9df6c 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -207,7 +207,6 @@ public override void Draw(IRenderer renderer) // Bind the custom shader. shader.Bind(); - // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. for (int i = 0; i < areas.Count; i++) { int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); diff --git a/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs index d262fc3140..f1dd5ba28f 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs @@ -71,18 +71,9 @@ public void Flush() if (changeBeginIndex == -1) return; - if (renderer.UseStructuredBuffers) - { - GL.BindBuffer(BufferTarget.UniformBuffer, Id); - GL.BufferSubData(BufferTarget.UniformBuffer, (IntPtr)(changeBeginIndex * elementSize), (IntPtr)(elementSize * changeCount), ref data[changeBeginIndex]); - GL.BindBuffer(BufferTarget.UniformBuffer, 0); - } - else - { - GL.BindBuffer(BufferTarget.UniformBuffer, Id); - GL.BufferSubData(BufferTarget.UniformBuffer, (IntPtr)(changeBeginIndex * elementSize), (IntPtr)(elementSize * changeCount), ref data[changeBeginIndex]); - GL.BindBuffer(BufferTarget.UniformBuffer, 0); - } + GL.BindBuffer(BufferTarget.UniformBuffer, Id); + GL.BufferSubData(BufferTarget.UniformBuffer, (IntPtr)(changeBeginIndex * elementSize), (IntPtr)(elementSize * changeCount), ref data[changeBeginIndex]); + GL.BindBuffer(BufferTarget.UniformBuffer, 0); changeBeginIndex = -1; changeCount = 0; diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs index 45e550735f..103caa6b87 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs @@ -187,7 +187,16 @@ private void compile() textureLayouts.Add(new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout))); } else - uniformLayouts[layout.Elements[0].Name] = new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout)); + { + switch (layout.Elements[0].Kind) + { + case ResourceKind.UniformBuffer: + case ResourceKind.StructuredBufferReadOnly: + case ResourceKind.StructuredBufferReadWrite: + uniformLayouts[layout.Elements[0].Name] = new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout)); + break; + } + } } Logger.Log(cached diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs index c776118447..1a2fdb4f58 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs @@ -122,8 +122,8 @@ private string loadFile(byte[]? bytes, bool mainFile) Inputs.AddRange( shader_input_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList()); - Outputs.AddRange(shader_output_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)) - .ToList()); + Outputs.AddRange( + shader_output_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList()); string outputCode = loadFile(store.GetRawData($"Internal/sh_{Type}_Output.h"), false); From 730cc387f48f3b15824514fa212dc92596ab536d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Aug 2023 12:17:09 +0900 Subject: [PATCH 14/18] Add comment on Clear() about preventing runaway VRAM usage --- .../Graphics/Rendering/ShaderStorageBufferObjectStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs index f93e94d872..765a1d8b84 100644 --- a/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -167,7 +167,7 @@ public void Pop() } /// - /// Clears the stack. + /// Clears the stack. This should be called at the start of every frame to prevent runaway VRAM usage. /// public void Clear() { From 1e91ef71afc12427ac157e1a2cbd4cd8898b1b76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Aug 2023 12:17:21 +0900 Subject: [PATCH 15/18] Add missing .Clear() call in test scene --- .../Visual/Graphics/TestSceneShaderStorageBufferObject.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 3189e9df6c..3086681f6d 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -202,6 +202,9 @@ public override void Draw(IRenderer renderer) // Create the SSBO. colourBuffer ??= new ShaderStorageBufferObjectStack(renderer, ubo_size, ssbo_size); + // Reset the SSBO. This should be called every frame. + colourBuffer.Clear(); + var rng = new Random(1337); // Bind the custom shader. From b4ee7dda6e8aaf9ddf92124441c0e8eae722ea22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Aug 2023 15:44:37 +0900 Subject: [PATCH 16/18] Update packages --- osu.Framework/osu.Framework.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 8bc2eee461..c5f367d58d 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -27,8 +27,8 @@ - - + + From 9ef363eb3f37c000747cc51080e442ca3338d151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Aug 2023 08:22:22 +0200 Subject: [PATCH 17/18] Remove unused field --- .../Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs index f1dd5ba28f..4dff9b5314 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs @@ -16,13 +16,10 @@ internal class GLShaderStorageBufferObject : IShaderStorageBufferObject Date: Tue, 15 Aug 2023 15:04:20 +0200 Subject: [PATCH 18/18] Fix SSBO usages crashing on Direct3D11 Co-authored-by: Dan Balasescu With the Direct3D11 surface active, opening `TestSceneShaderStorageBufferObject` would crash with: Exception Info: System.Runtime.InteropServices.SEHException (0x80004005): External component has thrown an exception. at Vortice.Direct3D11.ID3D11DeviceContext.DrawIndexed(Int32 indexCount, Int32 startIndexLocation, Int32 baseVertexLocation) at osu.Framework.Graphics.Veldrid.VeldridRenderer.DrawVertices(PrimitiveTopology type, Int32 vertexStart, Int32 verticesCount) in D:\Open Source\osu-framework\osu.Framework\Graphics\Veldrid\VeldridRenderer.cs:line 601 at osu.Framework.Graphics.Veldrid.Batches.VeldridVertexBatch`1.Draw() in D:\Open Source\osu-framework\osu.Framework\Graphics\Veldrid\Batches\VeldridVertexBatch.cs:line 161 at osu.Framework.Graphics.Veldrid.Batches.VeldridVertexBatch`1.Add(T v) in D:\Open Source\osu-framework\osu.Framework\Graphics\Veldrid\Batches\VeldridVertexBatch.cs:line 116 at osu.Framework.Tests.Visual.Graphics.TestSceneShaderStorageBufferObject.RawStorageBufferDrawNode.Draw(IRenderer renderer) in D:\Open Source\osu-framework\osu.Framework.Tests\Visual\Graphics\TestSceneShaderStorageBufferObject.cs:line 129 at osu.Framework.Graphics.Containers.CompositeDrawable.CompositeDrawableDrawNode.Draw(IRenderer renderer) in D:\Open Source\osu-framework\osu.Framework\Graphics\Containers\CompositeDrawable_DrawNode.cs:line 204 After some _extensive_ probing of this opaque error, an ETW trace turned up the following debug message: ID3D11DeviceContext::DrawIndexed: The Shader Resource View in slot 0 of the Pixel Shader unit was not created with the D3D11_BUFFEREX_SRV_FLAG_RAW flag, however the shader expects a RAW Buffer. This mismatch is invalid if the shader actually uses the view (e.g. it is not skipped due to shader code branching). As per the documentation (https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-intro#raw-views-of-buffers): You can think of a raw buffer, which can also be called a byte address buffer, as a bag of bits to which you want raw access, that is, a buffer that you can conveniently access through chunks of one to four 32-bit typeless address values. The HLSL shader code emitted by the cross-compiler is using one of those access methods, namely `Load4()`. To resolve, specify `rawBuffer: true` when creating the SSBO (in the `UseStructuredBuffers` path, which indicates they're supported). This flag only has an effect on D3D11, so it should come with no negative effects on other platforms. --- .../Veldrid/Buffers/VeldridShaderStorageBufferObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs b/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs index 4cfc4bd759..8f5949ce11 100644 --- a/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs +++ b/osu.Framework/Graphics/Veldrid/Buffers/VeldridShaderStorageBufferObject.cs @@ -27,7 +27,7 @@ public VeldridShaderStorageBufferObject(VeldridRenderer renderer, int uboSize, i if (renderer.UseStructuredBuffers) { Size = ssboSize; - buffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(elementSize * Size), BufferUsage.StructuredBufferReadOnly | BufferUsage.Dynamic, elementSize)); + buffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(elementSize * Size), BufferUsage.StructuredBufferReadOnly | BufferUsage.Dynamic, elementSize, true)); } else {