mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Merge branch 'fix/ssbo-rc13' into dev
This commit is contained in:
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 900b29d449...beaa9532a2
@@ -722,10 +722,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core table 23.4/23.5: *_BUFFER_SIZE reports the size glBindBufferRange
|
||||
// was ASKED for, verbatim. It is not clamped to the buffer's storage, and it does
|
||||
// not follow the buffer when a later glBufferData resizes it - a range may legally
|
||||
// name bytes the buffer does not have yet. Clamping it here answered 0 for the
|
||||
// common conformance shape of binding a range on a buffer that has no storage
|
||||
// yet (KHR-GL43.shader_storage_buffer_object.basic-binding).
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
||||
*data = static_cast<GLint>(end - start);
|
||||
*data = static_cast<GLint>(range.end - range.start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
@@ -951,9 +955,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
||||
*data = static_cast<GLint64>(end - start);
|
||||
// Verbatim, unclamped - see the GetIntegeri_v arm.
|
||||
*data = static_cast<GLint64>(range.end - range.start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -60,6 +60,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UniformInitializerScenario.cpp
|
||||
// Copyright (c) 2025-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
|
||||
//
|
||||
// Scenario - A DEFAULT-BLOCK UNIFORM'S DECLARED INITIALIZER.
|
||||
//
|
||||
// Desktop GLSL has allowed "uniform int i = 1;" since 1.20, and the initializer is not a
|
||||
// suggestion: it is the value the uniform reads until the application calls glUniform*, and
|
||||
// the value it goes back to after every relink. Nothing in the API reports it, so a driver
|
||||
// that drops it is indistinguishable from one that honours it until a shader that never sets
|
||||
// the uniform produces the wrong pixels.
|
||||
//
|
||||
// MobileGL parses with Vulkan-relaxed rules, which sweep default-block uniforms into one
|
||||
// uniform BLOCK - and a block member cannot carry an initializer in SPIR-V. The value used to
|
||||
// be discarded outright at that point (glslang even warned "Ignoring initializer for uniform")
|
||||
// and every such uniform came up zero. That is not a corner case: a large share of
|
||||
// KHR-GL43.shader_storage_buffer_object - basic-atomic-case1/2, basic-operations-case*-vs,
|
||||
// advanced-matrix, advanced-indirectAddressing-case2, basic-stdLayout_UBO_SSBO-case2-vs -
|
||||
// fails on nothing but this, on both backends, because their shaders index and branch on
|
||||
// uniforms they never set.
|
||||
//
|
||||
// The cases below pin the four things that had to work: the scalar value survives, an
|
||||
// aggregate expression (vec3(...), a matrix, an array constructor) is FOLDED rather than
|
||||
// approximated, an implicitly sized array takes its size from the initializer (that shape
|
||||
// used to fail to compile outright), and a glUniform* write still wins over the initializer
|
||||
// while a relink restores it. Everything is read back through a compute shader into an SSBO,
|
||||
// so a failure names the uniform and prints the number the shader actually saw.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Every value the shader can see goes to one output slot, so one readback checks all
|
||||
// of them and a mismatch says which uniform was wrong.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform int g_scalar = 7;
|
||||
uniform vec3 g_vector = vec3(10.0, 20.0, 30.0);
|
||||
uniform mat3 g_matrix = mat3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
|
||||
uniform int g_array[] = int[](11, 22, 33, 44);
|
||||
uniform uint g_unsigned = 3u;
|
||||
uniform bool g_flag = true;
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
int g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = g_scalar;
|
||||
g_out[1] = int(g_vector.x);
|
||||
g_out[2] = int(g_vector.y);
|
||||
g_out[3] = int(g_vector.z);
|
||||
// Column-major: [column][row]. Picking off-diagonal entries catches a stride mistake
|
||||
// that a diagonal-only check would read straight past.
|
||||
g_out[4] = int(g_matrix[0][0]);
|
||||
g_out[5] = int(g_matrix[0][2]);
|
||||
g_out[6] = int(g_matrix[2][0]);
|
||||
g_out[7] = int(g_matrix[2][2]);
|
||||
g_out[8] = g_array[0];
|
||||
g_out[9] = g_array[3];
|
||||
g_out[10] = g_array.length();
|
||||
g_out[11] = int(g_unsigned);
|
||||
g_out[12] = g_flag ? 1 : 0;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kOutputSlots = 13;
|
||||
|
||||
class UniformInitializerScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = CompileComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenBuffers(1, &m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
const std::vector<int> zeroes(kOutputSlots, 0);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(int), zeroes.data(), GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
std::vector<int> Dispatch() {
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<int> values(kOutputSlots, -1);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(int), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
return values;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_output = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(UniformInitializerScenario, AnUnsetUniformReadsItsDeclaredInitializer) {
|
||||
if (!Ready()) return;
|
||||
const std::vector<int> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
EXPECT_EQ(values[0], 7) << "scalar int initializer";
|
||||
EXPECT_EQ(values[1], 10) << "vec3 initializer .x";
|
||||
EXPECT_EQ(values[2], 20) << "vec3 initializer .y";
|
||||
EXPECT_EQ(values[3], 30) << "vec3 initializer .z";
|
||||
EXPECT_EQ(values[4], 1) << "mat3 initializer [0][0]";
|
||||
EXPECT_EQ(values[5], 3) << "mat3 initializer [0][2] - column stride";
|
||||
EXPECT_EQ(values[6], 7) << "mat3 initializer [2][0] - column stride";
|
||||
EXPECT_EQ(values[7], 9) << "mat3 initializer [2][2]";
|
||||
EXPECT_EQ(values[8], 11) << "array initializer element 0";
|
||||
EXPECT_EQ(values[9], 44) << "array initializer element 3";
|
||||
EXPECT_EQ(values[10], 4) << "implicitly sized array took its size from the initializer";
|
||||
EXPECT_EQ(values[11], 3) << "uint initializer";
|
||||
EXPECT_EQ(values[12], 1) << "bool initializer";
|
||||
}
|
||||
|
||||
TEST_F(UniformInitializerScenario, AnApplicationWriteBeatsTheInitializer) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "g_scalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "g_vector");
|
||||
const GLint element = glGetUniformLocation(m_program, "g_array[3]");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(element, 0);
|
||||
glUniform1i(scalar, 99);
|
||||
const float replacement[3] = {1.0f, 2.0f, 3.0f};
|
||||
glUniform3fv(vector, 1, replacement);
|
||||
glUniform1i(element, 55);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<int> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(values[0], 99);
|
||||
EXPECT_EQ(values[1], 1);
|
||||
EXPECT_EQ(values[3], 3);
|
||||
EXPECT_EQ(values[9], 55);
|
||||
// Untouched uniforms keep their initializers - a seed that only worked when
|
||||
// nothing else was written would pass the first case and still be wrong here.
|
||||
EXPECT_EQ(values[8], 11);
|
||||
EXPECT_EQ(values[11], 3);
|
||||
}
|
||||
|
||||
TEST_F(UniformInitializerScenario, RelinkingRestoresTheInitializer) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "g_scalar");
|
||||
ASSERT_GE(scalar, 0);
|
||||
glUniform1i(scalar, 1234);
|
||||
glUseProgram(0);
|
||||
ASSERT_EQ(Dispatch()[0], 1234);
|
||||
|
||||
glLinkProgram(m_program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
|
||||
const std::vector<int> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(values[0], 7) << "a relink puts every uniform back to its initializer";
|
||||
EXPECT_EQ(values[1], 10);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -301,6 +301,29 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
if (!ConsumeShaders(shaders)) return;
|
||||
|
||||
// Harvest the declared default-block uniform initializers before the TShaders are
|
||||
// handed to the linker. They come from the parse itself (glslang folds the constant
|
||||
// and hands it over instead of dropping it), not from a lexical scan, so an
|
||||
// expression like vec3(10, 20, 30) or int[](1, 2, 3) is already evaluated.
|
||||
//
|
||||
// Stage order decides a tie. GLSL requires a uniform declared in several stages to be
|
||||
// declared identically, initializer included, so a conflict is a malformed program;
|
||||
// taking the first stage's value keeps a link that other implementations accept from
|
||||
// failing here, and both stages agree in every well-formed one.
|
||||
for (const auto& shader : shaders) {
|
||||
const glslang::TIntermediate* intermediate = shader ? shader->getIntermediate() : nullptr;
|
||||
if (intermediate == nullptr) continue;
|
||||
for (const auto& initializer : intermediate->getUniformInitializers()) {
|
||||
const auto known = std::find_if(artifacts.uniformInitialValues.begin(),
|
||||
artifacts.uniformInitialValues.end(),
|
||||
[&initializer](const auto& existing) {
|
||||
return existing.name == initializer.name;
|
||||
});
|
||||
if (known != artifacts.uniformInitialValues.end()) continue;
|
||||
artifacts.uniformInitialValues.push_back(initializer);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the shaders' lexically extracted explicit uniform locations. The same
|
||||
// uniform declared in several stages must agree on its location (config-A glslang
|
||||
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
|
||||
|
||||
@@ -90,8 +90,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// A node that settled as Cancelled published nothing, so m_spirv stays empty with
|
||||
// spirvStatus false: linked, queryable, not drawable. Nothing to repair.
|
||||
|
||||
// Before the version bump, and before any caller can read the shadow: the writes the
|
||||
// application made while the layout did not exist yet.
|
||||
// Order matters, and it is the GL order. The shadow arrives zero-filled; the shaders'
|
||||
// declared uniform initializers are what it should actually start from, and only then
|
||||
// do the application's own writes - the ones it made while the layout did not exist
|
||||
// yet - land on top. Seeding after the replay would clobber them.
|
||||
ApplyUniformInitialValues();
|
||||
ReplayBufferedUniformWrites();
|
||||
|
||||
// The THIRD version bump of this link (enqueue, phase-A publish, phase-B publish), and
|
||||
@@ -126,6 +129,93 @@ namespace MobileGL::MG_State::GLState {
|
||||
return true;
|
||||
}
|
||||
|
||||
// "uniform vec3 v = vec3(10, 20, 30);" - legal desktop GLSL since 1.20, and the value is
|
||||
// what the uniform reads until glUniform* replaces it (and again after every relink).
|
||||
// MobileGL parses with Vulkan-relaxed rules, which sweep default-block uniforms into
|
||||
// MGL_GLOBAL_UBO; a block member cannot carry an initializer in SPIR-V, so glslang hands
|
||||
// the folded constants over as a side-channel (TIntermediate::getUniformInitializers) and
|
||||
// this is where they are honoured. Without it every such uniform silently read zero -
|
||||
// which is what half of KHR-GL43.shader_storage_buffer_object was actually failing on.
|
||||
//
|
||||
// Writes go straight into the shadow rather than through glUniform*: this runs INSIDE the
|
||||
// phase-B publish, so re-entering the join gate is not available, and the location space
|
||||
// reflection assigns (one location per array element) is all that is needed.
|
||||
void ProgramObject::ApplyUniformInitialValues() const {
|
||||
// Through the phase-A gate, not off m_artifacts directly: phase B can be joined by a
|
||||
// caller that has not read anything phase A publishes yet, and reading the raw field
|
||||
// there would find the PREVIOUS link's block (or an empty one) and drop every
|
||||
// initializer without a trace. Artifacts() is a no-op once phase A is in.
|
||||
const auto& initializers = Artifacts().uniformInitialValues;
|
||||
if (initializers.empty()) return;
|
||||
if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) {
|
||||
// Phase B published no shadow (cancelled, or superseded by a relink). The program
|
||||
// is not drawable; there is nowhere for these to land.
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
||||
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
||||
|
||||
for (const auto& init : initializers) {
|
||||
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
|
||||
// out column by column - which is also the order glslang folded them in.
|
||||
const Int columns = init.matrixCols;
|
||||
const Int rows = init.matrixRows;
|
||||
const Int componentsPerElement = columns > 0 ? columns * rows : init.vectorSize;
|
||||
const Int elements = init.arraySize;
|
||||
if (componentsPerElement <= 0 || elements <= 0) continue;
|
||||
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat || init.basicType == glslang::EbtFloat16;
|
||||
const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint ||
|
||||
init.basicType == glslang::EbtBool;
|
||||
// Anything else (fp64, 64-bit integers) has no 32-bit shadow encoding here, and a
|
||||
// half-written uniform is worse than an untouched one.
|
||||
if (!isFloat && !isInt) continue;
|
||||
const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size();
|
||||
if (provided < static_cast<SizeT>(componentsPerElement) * static_cast<SizeT>(elements)) continue;
|
||||
|
||||
const Int baseLocation = GetUniformLocation(init.name);
|
||||
if (baseLocation < 0) continue; // optimized away, or not a default-block uniform
|
||||
|
||||
for (Int element = 0; element < elements; ++element) {
|
||||
const Int location = baseLocation + element;
|
||||
if (element > 0 && !UniformLocationsAliasSameUniform(baseLocation, location)) break;
|
||||
if (!IsValidUniformLocation(location)) break;
|
||||
const Uint offset = GetUniformOffset(static_cast<Uint>(location));
|
||||
if (offset == kInvalidUniformOffset) continue;
|
||||
|
||||
// std140 pads every column of a float matrix out to a vec4, so the columns of
|
||||
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span
|
||||
// states the stride the rest of the pipeline agreed on rather than guessing it.
|
||||
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location));
|
||||
const SizeT columnStride =
|
||||
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
|
||||
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
|
||||
const Int columnCount = columns > 0 ? columns : 1;
|
||||
|
||||
for (Int column = 0; column < columnCount; ++column) {
|
||||
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
|
||||
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32);
|
||||
if (byteOffset + writeSize > uboSize) break;
|
||||
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
|
||||
static_cast<SizeT>(column) * componentsPerColumn;
|
||||
for (Int component = 0; component < componentsPerColumn; ++component) {
|
||||
const SizeT source = firstComponent + static_cast<SizeT>(component);
|
||||
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32);
|
||||
if (isFloat) {
|
||||
const Float value = static_cast<Float>(init.floatValues[source]);
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
} else {
|
||||
const Int32 value = static_cast<Int32>(init.intValues[source]);
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MarkUBOContentDirty();
|
||||
}
|
||||
|
||||
void ProgramObject::ReplayBufferedUniformWrites() const {
|
||||
if (m_pendingUniformWrites.empty()) {
|
||||
m_pendingUniformBytes.clear();
|
||||
@@ -234,6 +324,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.glBlockIndexToTProgram.clear();
|
||||
artifacts.tProgramBlockIndexToGl.clear();
|
||||
artifacts.linkedExplicitUniformLocations.clear();
|
||||
artifacts.uniformInitialValues.clear();
|
||||
artifacts.uniformIndexInTProgram.clear();
|
||||
artifacts.uniformSamplerOrImageUnitIndex.clear();
|
||||
artifacts.explicitOpaqueUniformBindings.clear();
|
||||
|
||||
@@ -756,6 +756,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
|
||||
// them from reflection; the DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
// reads until the application overwrites it, and relinking restores it - but the
|
||||
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
|
||||
// cannot carry an initializer, so the value only survives as this side-channel.
|
||||
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
|
||||
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
|
||||
UnorderedMap<String, Uint> uniformLocations;
|
||||
// Ordered by location,
|
||||
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||
@@ -1010,6 +1017,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// detour exactly - and a record that really does change bytes moves the version, which
|
||||
// is what makes a backend re-upload the UBO it cached during the window.
|
||||
void ReplayBufferedUniformWrites() const;
|
||||
// Seeds the freshly published uniform shadow with the declared initializers. Runs at
|
||||
// the phase-B publish, BEFORE ReplayBufferedUniformWrites, so an application write
|
||||
// made during the A->B window still wins - which is the GL ordering.
|
||||
void ApplyUniformInitialValues() const;
|
||||
// Past this, BufferUniformWrite declines and the write joins instead. Sized so an
|
||||
// ordinary pack load never reaches it (a pending window is one program's worth of
|
||||
// uniforms) while a pathological writer cannot grow the heap without bound.
|
||||
|
||||
@@ -480,6 +480,58 @@ TEST_F(BufferTest, BindBufferRangeZeroUnbindsBindingPoint) {
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core tables 23.4/23.5: *_BUFFER_START and *_BUFFER_SIZE report the (offset, size) pair
|
||||
// glBindBufferRange was ASKED for. They are not clamped to the buffer's storage - a range may
|
||||
// legally name bytes the buffer does not have, and glBufferData may resize the buffer afterwards
|
||||
// without the binding's reported window moving. The size arm used to intersect the recorded range
|
||||
// with the buffer's current size, so binding a range on a still-empty buffer (glGenBuffers with no
|
||||
// glBufferData - exactly what KHR-GL43.shader_storage_buffer_object.basic-binding does) answered 0
|
||||
// while START still answered the offset, an internally inconsistent pair no driver reports.
|
||||
TEST_F(BufferTest, IndexedBufferSizeQueryReportsTheRequestedSizeNotTheBuffersStorage) {
|
||||
GLint ssboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
ASSERT_GT(ssboAlignment, 0);
|
||||
const GLintptr offset = ssboAlignment;
|
||||
const GLsizeiptr size = 512;
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
// Deliberately no glBufferData: the name exists, the storage does not.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, offset, size);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint start32 = 0;
|
||||
GLint size32 = 0;
|
||||
GLint64 start64 = 0;
|
||||
GLint64 size64 = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start32);
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
|
||||
MobileGL::MG_Impl::GLImpl::GetInteger64i_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start64);
|
||||
MobileGL::MG_Impl::GLImpl::GetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size64);
|
||||
EXPECT_EQ(start32, static_cast<GLint>(offset));
|
||||
EXPECT_EQ(size32, static_cast<GLint>(size));
|
||||
EXPECT_EQ(start64, static_cast<GLint64>(offset));
|
||||
EXPECT_EQ(size64, static_cast<GLint64>(size));
|
||||
|
||||
// Giving the buffer storage afterwards does not move the window either way.
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, offset + size, nullptr, GL_DYNAMIC_DRAW);
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
|
||||
EXPECT_EQ(size32, static_cast<GLint>(size));
|
||||
|
||||
// glBindBufferBase binds the whole buffer and reports (0, 0), not the buffer's size.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start32);
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
|
||||
EXPECT_EQ(start32, 0);
|
||||
EXPECT_EQ(size32, 0);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, GetInteger64vMaxShaderStorageBlockSize) {
|
||||
GLint64 maxSsboBlockSize = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetInteger64v(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &maxSsboBlockSize);
|
||||
|
||||
@@ -367,6 +367,7 @@ public:
|
||||
|
||||
TIntermTyped* vkRelaxedRemapFunctionCall(const TSourceLoc&, TFunction*, TIntermNode*);
|
||||
// returns true if the variable was remapped to something else
|
||||
void recordUniformInitializer(const TString&, const TType&, const TConstUnionArray&);
|
||||
bool vkRelaxedRemapUniformVariable(const TSourceLoc&, TString&, const TPublicType&, TArraySizes*, TIntermTyped*, TType&);
|
||||
void vkRelaxedRemapUniformMembers(const TSourceLoc&, const TPublicType&, const TType&, const TString&);
|
||||
void vkRelaxedRemapFunctionParameter(TFunction*, TParameter&, std::vector<int>* newParams = nullptr);
|
||||
|
||||
@@ -611,6 +611,32 @@ public:
|
||||
void setGlobalUniformBinding(unsigned int binding) { globalUniformBlockBinding = binding; }
|
||||
unsigned int getGlobalUniformBinding() const { return globalUniformBlockBinding; }
|
||||
|
||||
// A default-block uniform's initializer, folded to constants at parse time.
|
||||
//
|
||||
// Desktop GLSL 1.20+ lets a default-block uniform carry an initializer, and that value is
|
||||
// what the uniform reads until the application overwrites it with glUniform*. Vulkan-relaxed
|
||||
// parsing sweeps such uniforms into a uniform BLOCK, and a block member cannot carry an
|
||||
// initializer in SPIR-V - so the value has nowhere to live in the generated module and used
|
||||
// to be dropped outright, leaving the uniform silently zero. The CLIENT is the only party
|
||||
// that can still honor it, by writing the value into the block's backing storage once the
|
||||
// program links, so the folded constants are handed out here instead of discarded.
|
||||
//
|
||||
// Scalars appear in the same flattened order glslang folds them in: array element by array
|
||||
// element, and within a matrix, column by column. Exactly one of the two value vectors is
|
||||
// populated, chosen by basicType.
|
||||
struct TUniformInitializer {
|
||||
std::string name;
|
||||
TBasicType basicType = EbtVoid;
|
||||
int vectorSize = 1; // components per vector; 1 for a scalar
|
||||
int matrixCols = 0; // 0 when the type is not a matrix
|
||||
int matrixRows = 0;
|
||||
int arraySize = 1; // outer array element count; 1 when not an array
|
||||
std::vector<long long> intValues;
|
||||
std::vector<double> floatValues;
|
||||
};
|
||||
void addUniformInitializer(TUniformInitializer&& init) { uniformInitializers.push_back(std::move(init)); }
|
||||
const std::vector<TUniformInitializer>& getUniformInitializers() const { return uniformInitializers; }
|
||||
|
||||
void setAtomicCounterBlockName(const char* name) { atomicCounterBlockName = std::string(name); }
|
||||
const char* getAtomicCounterBlockName() const { return atomicCounterBlockName.c_str(); }
|
||||
void setAtomicCounterBlockSet(unsigned int set) { atomicCounterBlockSet = set; }
|
||||
@@ -1223,6 +1249,7 @@ protected:
|
||||
|
||||
std::string globalUniformBlockName;
|
||||
std::string atomicCounterBlockName;
|
||||
std::vector<TUniformInitializer> uniformInitializers;
|
||||
unsigned int globalUniformBlockSet;
|
||||
unsigned int globalUniformBlockBinding;
|
||||
unsigned int atomicCounterBlockSet;
|
||||
|
||||
Reference in New Issue
Block a user