Skip to content

Commit

Permalink
Merge pull request #5950 from smoogipoo/shader-storage-buffer-objects
Browse files Browse the repository at this point in the history
Add support for shader storage buffer objects (SSBOs)
  • Loading branch information
bdach authored Aug 15, 2023
2 parents 6799b1f + 7cc4dbe commit 91b82bc
Show file tree
Hide file tree
Showing 24 changed files with 1,201 additions and 47 deletions.
258 changes: 258 additions & 0 deletions osu.Framework.Tests/Graphics/ShaderStorageBufferObjectStackTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<int> stack = null!;

[SetUp]
public void Setup()
{
stack = new ShaderStorageBufferObjectStack<int>(new DummyRenderer(), 2, size);
}

[Test]
public void TestBufferMustBeAtLeast2Elements()
{
Assert.Throws<ArgumentOutOfRangeException>(() => _ = new ShaderStorageBufferObjectStack<int>(new DummyRenderer(), 1, 100));
Assert.Throws<ArgumentOutOfRangeException>(() => _ = new ShaderStorageBufferObjectStack<int>(new DummyRenderer(), 100, 1));
Assert.DoesNotThrow(() => _ = new ShaderStorageBufferObjectStack<int>(new DummyRenderer(), 2, 100));
Assert.DoesNotThrow(() => _ = new ShaderStorageBufferObjectStack<int>(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<InvalidOperationException>(() => 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();
}
}
}
}
35 changes: 35 additions & 0 deletions osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.fs
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions osu.Framework.Tests/Resources/Shaders/sh_SSBOTest.vs
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 91b82bc

Please sign in to comment.