diff --git a/3rdparty/glslang b/3rdparty/glslang index 900b29d4..beaa9532 160000 --- a/3rdparty/glslang +++ b/3rdparty/glslang @@ -1 +1 @@ -Subproject commit 900b29d449a67d2a18b569f64dd46333575b352f +Subproject commit beaa9532a20832e5a310a7f85ed330e6a7db6898 diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index f723c3e4..d58e2d0f 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -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(end - start); + *data = static_cast(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(end - start); + // Verbatim, unclamped - see the GetIntegeri_v arm. + *data = static_cast(range.end - range.start); return; } default: diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index bf43aff9..2359f348 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 ) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/UniformInitializerScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/UniformInitializerScenario.cpp new file mode 100644 index 00000000..d21adfc9 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/UniformInitializerScenario.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 +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 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 Dispatch() { + glUseProgram(m_program); + glDispatchCompute(1, 1, 1); + glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + std::vector 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 values = Dispatch(); + EXPECT_EQ(glGetError(), static_cast(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 values = Dispatch(); + EXPECT_EQ(glGetError(), static_cast(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 values = Dispatch(); + EXPECT_EQ(glGetError(), static_cast(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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index ebb0be57..d0cd328e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -301,6 +301,29 @@ namespace MobileGL::MG_State::GLState { Vector> 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). diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 4a54eda9..0a572a3e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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(componentsPerElement) * static_cast(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(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(location)); + const SizeT columnStride = + columns > 0 ? slotSpan / static_cast(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(offset) + static_cast(column) * columnStride; + const SizeT writeSize = static_cast(componentsPerColumn) * sizeof(Uint32); + if (byteOffset + writeSize > uboSize) break; + const SizeT firstComponent = static_cast(element) * componentsPerElement + + static_cast(column) * componentsPerColumn; + for (Int component = 0; component < componentsPerColumn; ++component) { + const SizeT source = firstComponent + static_cast(component); + Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32); + if (isFloat) { + const Float value = static_cast(init.floatValues[source]); + std::memcpy(destination, &value, sizeof(value)); + } else { + const Int32 value = static_cast(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(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 482166e3..6bc49abc 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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 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 uniformInitialValues; UnorderedMap 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. diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index b62bdc9e..04084a20 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -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(offset)); + EXPECT_EQ(size32, static_cast(size)); + EXPECT_EQ(start64, static_cast(offset)); + EXPECT_EQ(size64, static_cast(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(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); diff --git a/include/glslang/MachineIndependent/ParseHelper.h b/include/glslang/MachineIndependent/ParseHelper.h index a2de165e..42bc00c7 100644 --- a/include/glslang/MachineIndependent/ParseHelper.h +++ b/include/glslang/MachineIndependent/ParseHelper.h @@ -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* newParams = nullptr); diff --git a/include/glslang/MachineIndependent/localintermediate.h b/include/glslang/MachineIndependent/localintermediate.h index 0d299bda..e19bc4d7 100644 --- a/include/glslang/MachineIndependent/localintermediate.h +++ b/include/glslang/MachineIndependent/localintermediate.h @@ -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 intValues; + std::vector floatValues; + }; + void addUniformInitializer(TUniformInitializer&& init) { uniformInitializers.push_back(std::move(init)); } + const std::vector& 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 uniformInitializers; unsigned int globalUniformBlockSet; unsigned int globalUniformBlockBinding; unsigned int atomicCounterBlockSet;