diff --git a/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs new file mode 100644 index 0000000000..a8561e3cc7 --- /dev/null +++ b/osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs @@ -0,0 +1,258 @@ +// 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.CurrentOffset, Is.Zero); + Assert.That(stack.CurrentBuffer, Is.Not.Null); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], 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.CurrentOffset, Is.Zero); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], 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.CurrentOffset, Is.EqualTo(expectedIndex++)); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], 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.CurrentOffset, Is.EqualTo(i)); + Assert.That(stack.CurrentBuffer, Is.EqualTo(firstBuffer)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], 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.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.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. + 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.CurrentOffset]; + + // 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.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.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.CurrentOffset, 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.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.CurrentOffset, 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.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.CurrentOffset, 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.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.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)); + + // 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.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.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... + 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.CurrentOffset, Is.EqualTo(size - 3)); + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], 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.CurrentOffset - 1], Is.EqualTo(0)); + + // Test that the item was correctly placed in the new buffer + Assert.That(stack.CurrentBuffer[stack.CurrentOffset], Is.EqualTo(i)); + + // Return to an empty stack. + stack.Pop(); + } + } + } +} 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..23c4835300 --- /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..3086681f6d --- /dev/null +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -0,0 +1,280 @@ +// 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 NUnit.Framework; +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 + { + 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() + { + AddStep("add grid", () => Child = new GridDrawable + { + RelativeSizeAxes = Axes.Both + }); + } + + private partial class GridDrawable : Drawable + { + private const int separation = 1; + private const int size = 32; + + public bool RawBuffer; + + public IShader Shader { get; private set; } = null!; + public List Areas { get; } = 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() => 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) + { + } + + 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(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) }; + } + + // 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(); + } + } + + /// + /// 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); + + // Reset the SSBO. This should be called every frame. + colourBuffer.Clear(); + + var rng = new Random(1337); + + // Bind the custom shader. + shader.Bind(); + + for (int i = 0; i < areas.Count; i++) + { + int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); + + // 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 + { + Position = areas[i].BottomLeft, + ColourIndex = colourIndex + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].BottomRight, + ColourIndex = colourIndex + }); + + vertices.Add(new ColourIndexedVertex + { + Position = areas[i].TopRight, + ColourIndex = colourIndex + }); + + vertices.Add(new ColourIndexedVertex + { + 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(); + } + + 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/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..4dff9b5314 --- /dev/null +++ b/osu.Framework/Graphics/OpenGL/Buffers/GLShaderStorageBufferObject.cs @@ -0,0 +1,84 @@ +// 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 uint elementSize; + + public GLShaderStorageBufferObject(GLRenderer renderer, int uboSize, int ssboSize) + { + 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; + + 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/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/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/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 41878f229c..f864712d88 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 { @@ -44,6 +46,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. /// @@ -56,8 +60,7 @@ protected internal override bool VerticalSync private int backbufferFramebuffer; - private readonly int[] lastBoundBuffers = new int[2]; - + private readonly Dictionary boundUniformBuffers = new Dictionary(); private bool? lastBlendingEnabledState; private int lastBoundVertexArray; @@ -80,12 +83,18 @@ 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; + + Logger.Log($"{nameof(UseStructuredBuffers)}: {UseStructuredBuffers}"); openGLSurface.ClearCurrent(); } @@ -107,8 +116,8 @@ protected virtual string GetExtensions() protected internal override void BeginFrame(Vector2 windowSize) { lastBlendingEnabledState = null; - lastBoundBuffers.AsSpan().Clear(); lastBoundVertexArray = 0; + boundUniformBuffers.Clear(); // Seems to be required on some drivers as the context is lost from the draw thread. MakeCurrent(); @@ -139,19 +148,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) @@ -273,12 +269,45 @@ protected override void ClearImplementation(ClearInfo clearInfo) GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); } + public void BindUniformBuffer(string blockName, IGLUniformBuffer glBuffer) + { + if (boundUniformBuffers.TryGetValue(blockName, out IGLUniformBuffer? current) && current == glBuffer) + return; + + 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))); } @@ -440,7 +469,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/OpenGL/Shaders/GLShader.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs index db00256190..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) @@ -187,10 +189,19 @@ private protected virtual bool CompileInternal() Value = textureIndex++ }); } - else if (layout.Elements[0].Kind == ResourceKind.UniformBuffer) + else { - var block = new GLUniformBlock(this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++); - uniformBlocks[layout.Elements[0].Name] = block; + 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; + } } } diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs index a73ec2605f..783c3e38f2 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/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..dc55c792df --- /dev/null +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyShaderStorageBufferObject.cs @@ -0,0 +1,31 @@ +// 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 + { + private readonly T[] data; + + public DummyShaderStorageBufferObject(int size) + { + Size = size; + data = new T[size]; + } + + public int Size { get; } + + public T this[int index] + { + get => data[index]; + set => data[index] = value; + } + + public void Dispose() + { + } + } +} 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/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 a552f18cf7..6fbfaf88fd 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -1075,6 +1075,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 . /// @@ -1186,11 +1189,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."); @@ -1220,7 +1235,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..765a1d8b84 --- /dev/null +++ b/osu.Framework/Graphics/Rendering/ShaderStorageBufferObjectStack.cs @@ -0,0 +1,191 @@ +// 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 CurrentOffset => currentBufferOffset; + + /// + /// The buffer that contains the current object. + /// + 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(); + + /// + /// 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 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.StorageBufferOverflow); + + // + // 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 newIndex = lastIndices.Pop(); + int newBufferIndex = newIndex / bufferSize; + + // Flush the pipeline if this invocation transitions to a new buffer. + if (newBufferIndex != currentBufferIndex) + renderer.FlushCurrentBatch(FlushBatchSource.StorageBufferOverflow); + + currentIndex = newIndex; + } + + /// + /// Clears the stack. This should be called at the start of every frame to prevent runaway VRAM usage. + /// + 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..8f5949ce11 --- /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, true)); + } + 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/Shaders/VeldridShader.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs index bf0ab101cc..103caa6b87 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs @@ -186,8 +186,17 @@ private void compile() textureLayouts.Add(new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout))); } - else if (layout.Elements[0].Kind == ResourceKind.UniformBuffer) - uniformLayouts[layout.Elements[0].Name] = new VeldridUniformLayout(set, renderer.Factory.CreateResourceLayout(layout)); + else + { + 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 9bccdd13e9..807cdcc6bc 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); // Increment the binding set of all uniform blocks. // After this transformation, the g_GlobalUniforms block is placed in set 0 and all other user blocks begin from 1. @@ -105,8 +111,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 14b339376c..57ce6ad3ec 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. /// @@ -219,6 +221,8 @@ protected override void Initialise(IGraphicsSurface graphicsSurface) break; } + Logger.Log($"{nameof(UseStructuredBuffers)}: {UseStructuredBuffers}"); + MaxTextureSize = maxTextureSize; Commands = Factory.CreateCommandList(); @@ -524,6 +528,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; } @@ -730,7 +737,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, ShaderCompilationStore compilationStore) => new VeldridShader(this, name, parts.Cast().ToArray(), compilationStore); @@ -753,6 +760,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); 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 @@ - - + +