mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
298 lines
15 KiB
C++
298 lines
15 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp
|
|
// Copyright (c) 2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
//
|
|
// P8-SV: THE MAGMA WIRE ARM ISSUES glDispatchComputeIndirect FROM THE WIRE STORE, AS THE MONOLITH
|
|
// ARM DOES (docs/archive/notes/p8/SV.md).
|
|
//
|
|
// Before P8-SV the wire arm read the three group counts through ReadWireBuffer and dispatched them
|
|
// from the CPU, so a dispatch buffer a shader had written cost a whole-GPU wait per call - the
|
|
// dispatch twin of what P8-D removed for indirect draws (WireIndirectDrawScenario.cpp). The cases
|
|
// pin both halves:
|
|
// * THE GRID, on every arm and both backends - each dispatch marks exactly the work groups its
|
|
// counts name, and GPU-written counts are the ones the dispatch sees (the buffer starts with
|
|
// OTHER counts, so a stale read shows as the wrong grid);
|
|
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on the
|
|
// inproc arm: every call issued as vkCmdDispatchIndirect (`wdsp`), no host wait (`whw`), and the
|
|
// INDIRECT_COMMAND_READ barrier exactly once per shader write (`wibar`).
|
|
// The counters are read BEFORE the grids are read back: that readback is a host read of a store a
|
|
// shader wrote, so it takes a host wait of its own, which is the application's and not the
|
|
// dispatch's.
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "../Harness/HeadlessGL.h"
|
|
#include "../Harness/ScenarioFixture.h"
|
|
#include "../Harness/SplitRuntimePeek.h"
|
|
#include "../Harness/WireIndirectPeek.h"
|
|
|
|
#ifdef GLAPI
|
|
#undef GLAPI
|
|
#endif
|
|
#define GL_GLEXT_PROTOTYPES
|
|
#include <GL/gl.h>
|
|
#include <GL/glext.h>
|
|
|
|
namespace MGITest {
|
|
namespace {
|
|
|
|
// Grids up to 4 x 4 x 2: one cell per work group.
|
|
constexpr std::uint32_t kGridX = 4, kGridY = 4, kGridZ = 2;
|
|
constexpr std::uint32_t kCells = kGridX * kGridY * kGridZ;
|
|
|
|
// One invocation per work group, and each group marks its own cell.
|
|
constexpr const char* kMarkSource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 2) buffer Marks { uint marks[]; };
|
|
void main() {
|
|
uvec3 g = gl_WorkGroupID;
|
|
atomicAdd(marks[(g.z * 4u + g.y) * 4u + g.x], 1u);
|
|
}
|
|
)";
|
|
// Copies `source` into `words`, one word per invocation: the compute write under test.
|
|
constexpr const char* kCopySource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 0) buffer Words { uint words[]; };
|
|
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
|
|
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
|
|
)";
|
|
|
|
using Grid = std::array<std::uint32_t, 3>;
|
|
|
|
class WireIndirectDispatchScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
m_magma = Gl().BackendName() == "DirectVulkan";
|
|
m_transport = PeekSplitRuntime().transportName;
|
|
m_recordArm = PeekSplitRuntime().dataArmIsRecord;
|
|
m_mark = CompileCompute(kMarkSource);
|
|
ASSERT_NE(m_mark, 0u);
|
|
m_copy = CompileCompute(kCopySource);
|
|
ASSERT_NE(m_copy, 0u);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
|
// The counters are armed by the first peek, so it has to come before any dispatch.
|
|
if (CountersReadable()) {
|
|
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
|
|
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
|
|
}
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0);
|
|
for (GLuint& buffer : m_marks) {
|
|
if (buffer != 0) glDeleteBuffers(1, &buffer);
|
|
}
|
|
for (GLuint* buffer : {&m_groupCounts, &m_source}) {
|
|
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
|
*buffer = 0;
|
|
}
|
|
if (m_mark != 0) glDeleteProgram(m_mark);
|
|
if (m_copy != 0) glDeleteProgram(m_copy);
|
|
}
|
|
|
|
static GLuint CompileCompute(const char* source) {
|
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
glShaderSource(shader, 1, &source, nullptr);
|
|
glCompileShader(shader);
|
|
GLint ok = GL_FALSE;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
|
if (ok != GL_TRUE) {
|
|
glDeleteShader(shader);
|
|
return 0;
|
|
}
|
|
const GLuint program = glCreateProgram();
|
|
glAttachShader(program, shader);
|
|
glLinkProgram(program);
|
|
glDeleteShader(shader);
|
|
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
|
if (ok != GL_TRUE) {
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
static void Fill(GLuint& name, GLenum target, const std::vector<std::uint32_t>& words) {
|
|
if (name == 0) glGenBuffers(1, &name);
|
|
glBindBuffer(target, name);
|
|
glBufferData(target, static_cast<GLsizeiptr>(words.size() * sizeof(std::uint32_t)), words.data(),
|
|
GL_DYNAMIC_COPY);
|
|
}
|
|
|
|
// The dispatch-indirect store, holding CPU-written counts.
|
|
void Parameters(const std::vector<std::uint32_t>& words) {
|
|
Fill(m_groupCounts, GL_DISPATCH_INDIRECT_BUFFER, words);
|
|
}
|
|
|
|
// The compute write: the parameter store (already holding its poison) receives `words`.
|
|
void ComputeWriteParameters(const std::vector<std::uint32_t>& words) {
|
|
Fill(m_source, GL_SHADER_STORAGE_BUFFER, words);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_groupCounts);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
|
|
glUseProgram(m_copy);
|
|
glDispatchCompute(static_cast<GLuint>(words.size()), 1, 1);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
|
}
|
|
|
|
// One indirect dispatch of the mark program into a fresh, zeroed cell buffer. Returns
|
|
// the index of that buffer for ExpectGrid.
|
|
size_t DispatchIndirect(GLintptr offset) {
|
|
GLuint marks = 0;
|
|
Fill(marks, GL_SHADER_STORAGE_BUFFER, std::vector<std::uint32_t>(kCells, 0u));
|
|
m_marks.push_back(marks);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, marks);
|
|
glUseProgram(m_mark);
|
|
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_groupCounts);
|
|
glDispatchComputeIndirect(offset);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0);
|
|
return m_marks.size() - 1;
|
|
}
|
|
|
|
// A round trip that reads no buffer store, so the server has applied every dispatch
|
|
// above before the counters are read - and no host wait of its own is added.
|
|
static void Settle() {
|
|
BindDefaultFramebuffer();
|
|
(void)ReadPixels(1, 1);
|
|
}
|
|
|
|
void ExpectGrid(size_t which, const Grid& grid, const std::string& what) {
|
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
std::vector<std::uint32_t> cells(kCells, 0xDEADu);
|
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_marks[which]);
|
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, static_cast<GLsizeiptr>(kCells * sizeof(std::uint32_t)),
|
|
cells.data());
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << what << ": readback";
|
|
size_t wrong = 0;
|
|
std::string first;
|
|
for (std::uint32_t z = 0; z < kGridZ; ++z) {
|
|
for (std::uint32_t y = 0; y < kGridY; ++y) {
|
|
for (std::uint32_t x = 0; x < kGridX; ++x) {
|
|
const bool inside = x < grid[0] && y < grid[1] && z < grid[2];
|
|
const std::uint32_t got = cells[(z * kGridY + y) * kGridX + x];
|
|
if (got != (inside ? 1u : 0u) && wrong++ == 0) {
|
|
first = "group (" + std::to_string(x) + "," + std::to_string(y) + "," +
|
|
std::to_string(z) + ") = " + std::to_string(got);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EXPECT_EQ(wrong, 0u) << what << ": expected exactly the " << grid[0] << "x" << grid[1] << "x"
|
|
<< grid[2] << " grid marked once; first wrong " << first;
|
|
}
|
|
|
|
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
|
|
// processes, and the monolith arm never takes the wire path at all.
|
|
// The counters are this process's: inproc, and (P13 W4) monolith on the record arm.
|
|
bool CountersReadable() const {
|
|
return m_magma && (m_transport == "inproc" || (m_transport == "monolith" && m_recordArm));
|
|
}
|
|
|
|
WireIndirectCounters Delta() const {
|
|
WireIndirectCounters now{};
|
|
EXPECT_TRUE(PeekWireIndirectCounters(&now));
|
|
WireIndirectCounters delta{};
|
|
delta.nativeDraws = now.nativeDraws - m_before.nativeDraws;
|
|
delta.cpuExpansions = now.cpuExpansions - m_before.cpuExpansions;
|
|
delta.barriers = now.barriers - m_before.barriers;
|
|
delta.hostWaits = now.hostWaits - m_before.hostWaits;
|
|
delta.indirectWaits = now.indirectWaits - m_before.indirectWaits;
|
|
delta.nativeDispatches = now.nativeDispatches - m_before.nativeDispatches;
|
|
return delta;
|
|
}
|
|
|
|
bool m_magma = false;
|
|
std::string m_transport;
|
|
bool m_recordArm = false;
|
|
WireIndirectCounters m_before{};
|
|
GLuint m_mark = 0, m_copy = 0, m_groupCounts = 0, m_source = 0;
|
|
std::vector<GLuint> m_marks;
|
|
};
|
|
|
|
// CPU-written counts at two non-zero offsets of one store. On Magma/inproc both calls must
|
|
// be issued natively, with no host wait and no barrier (no shader wrote the store).
|
|
TEST_F(WireIndirectDispatchScenario, CpuWrittenGroupCountsAreReadByTheGpuAtTheirOffsets) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
Parameters({1, 1, 1, 3, 2, 1, 4, 1, 2});
|
|
const size_t first = DispatchIndirect(12);
|
|
const size_t second = DispatchIndirect(24);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
Settle();
|
|
if (CountersReadable()) {
|
|
const WireIndirectCounters delta = Delta();
|
|
EXPECT_EQ(delta.nativeDispatches, 2u) << "every indirect dispatch must be vkCmdDispatchIndirect";
|
|
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write to the counts";
|
|
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote this store, so no barrier is due";
|
|
}
|
|
ExpectGrid(first, {3, 2, 1}, "glDispatchComputeIndirect at byte 12");
|
|
ExpectGrid(second, {4, 1, 2}, "glDispatchComputeIndirect at byte 24");
|
|
}
|
|
|
|
// Counts a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT. The
|
|
// store starts with OTHER counts, so a stale read dispatches the wrong grid. On
|
|
// Magma/inproc: issued natively and - the point of the change - NO host wait.
|
|
TEST_F(WireIndirectDispatchScenario, ComputeWrittenGroupCountsDispatchWithoutAHostWait) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
Parameters({1, 1, 1});
|
|
ComputeWriteParameters({3, 2, 1});
|
|
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
|
const size_t first = DispatchIndirect(0);
|
|
|
|
Parameters({1, 1, 1, 1, 1, 1});
|
|
ComputeWriteParameters({1, 1, 1, 2, 2, 2});
|
|
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
|
|
const size_t second = DispatchIndirect(12);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
Settle();
|
|
if (CountersReadable()) {
|
|
const WireIndirectCounters delta = Delta();
|
|
EXPECT_EQ(delta.nativeDispatches, 2u) << "both dispatches must be issued natively";
|
|
EXPECT_EQ(delta.hostWaits, 0u)
|
|
<< "an indirect dispatch of shader-written counts waited for the GPU on the host";
|
|
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
|
|
}
|
|
ExpectGrid(first, {3, 2, 1}, "compute-written counts at byte 0");
|
|
ExpectGrid(second, {2, 2, 2}, "compute-written counts at byte 12");
|
|
}
|
|
|
|
// Flywheel's shape, one call over: the counts are written and dispatched with no
|
|
// glMemoryBarrier between (GL leaves it undefined). The wire arm orders it anyway, once per
|
|
// write, as P8-D does for draws. Magma's split arms only: the monolith arm records no
|
|
// dependency here (P8-D recorded that half for dev), and Espryt's is its driver's.
|
|
TEST_F(WireIndirectDispatchScenario, ShaderWrittenGroupCountsWithoutAnApplicationBarrierAreOrdered) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!m_magma || !m_recordArm) {
|
|
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D, P8-SV)";
|
|
}
|
|
Parameters({1, 1, 1});
|
|
ComputeWriteParameters({3, 2, 1});
|
|
const size_t first = DispatchIndirect(0);
|
|
const size_t again = DispatchIndirect(0);
|
|
ComputeWriteParameters({1, 2, 2});
|
|
const size_t third = DispatchIndirect(0);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
Settle();
|
|
if (CountersReadable()) {
|
|
const WireIndirectCounters delta = Delta();
|
|
EXPECT_EQ(delta.nativeDispatches, 3u);
|
|
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
|
|
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second dispatch reads no new write";
|
|
}
|
|
ExpectGrid(first, {3, 2, 1}, "the first dispatch after the write");
|
|
ExpectGrid(again, {3, 2, 1}, "a second dispatch, no write between");
|
|
ExpectGrid(third, {1, 2, 2}, "the dispatch after a second write");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|