mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[Fix, Test] (MG_State, Integration): move the framebuffer aggregate from a texture's or renderbuffer's storage definition - set_framebuffer_state inlines an attachment's format and a respecify while attached left Espryt's handle arm answering its four cross-object masks from the stale copy (P4a seam F-3)
This commit is contained in:
@@ -54,6 +54,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Harness/BackendCapsPeek.cpp
|
||||
Harness/PipeSlotPeek.cpp
|
||||
Harness/PipeApplyPeek.cpp
|
||||
Harness/P4aSeamPeek.cpp
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
@@ -137,6 +138,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
|
||||
Scenarios/TextureUploadShapeScenario.cpp
|
||||
Scenarios/ObjectSubsystemControlScenario.cpp
|
||||
Scenarios/P4aSeamAuditScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.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
|
||||
|
||||
#include "P4aSeamPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#define MGITEST_P4A_SEAM_PEEK_LIVE 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
#if defined(MGITEST_P4A_SEAM_PEEK_LIVE)
|
||||
namespace {
|
||||
namespace MGP = MobileGL::MG_Pipe;
|
||||
namespace MGB = MobileGL::MG_Backend::DirectGLES;
|
||||
|
||||
// "Is Espryt the backend running" - the same test PipeApplyPeek.cpp makes through a twin:
|
||||
// on Magma no ES entry point was ever resolved and every member of g_GLESFuncs is null.
|
||||
// It is asked BEFORE SamplerSubsystemEnabled(), which is Espryt's own latch and must not
|
||||
// be resolved on a process whose backend is not Espryt.
|
||||
bool EsprytIsRunning() { return MGB::g_GLESFuncs.glBindSampler != nullptr; }
|
||||
} // namespace
|
||||
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive) {
|
||||
if (outLive == nullptr) return false;
|
||||
if (!EsprytIsRunning()) return false;
|
||||
*outLive = MGB::SamplerSubsystemEnabled();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
out->Start = static_cast<unsigned>(applier.ShaderImageStart);
|
||||
out->Count = static_cast<unsigned>(applier.ShaderImageCount);
|
||||
out->Serial = static_cast<unsigned long long>(applier.ShaderImagesSerial);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out) {
|
||||
if (out == nullptr) return false;
|
||||
if (!EsprytIsRunning()) return false;
|
||||
if (!MobileGL::MG_State::pGLContext) return false;
|
||||
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||
if (unit >= applier.BoundSamplerStates.size() || unit >= MGB::SamplerImpl::g_boundSamplersCache.size()) {
|
||||
return false;
|
||||
}
|
||||
*out = EsprytUnitSamplerPeek{};
|
||||
|
||||
// Espryt's own binding shadow: every glBindSampler this backend issues routes through it
|
||||
// (BackendSamplerObject::Bind / UnbindSampler), so it IS what the driver holds.
|
||||
if (MGB::SamplerImpl::BackendSamplerObject* const bound = MGB::SamplerImpl::g_boundSamplersCache[unit]) {
|
||||
out->BoundSamplerId = static_cast<unsigned>(bound->GetBackendSamplerId());
|
||||
}
|
||||
|
||||
const MGP::MGPipeHandle cso = applier.BoundSamplerStates[unit];
|
||||
out->CsoHandleSlot = static_cast<unsigned>(cso.Slot);
|
||||
out->CsoHandleGen = static_cast<unsigned>(cso.Gen);
|
||||
out->UnitInsideWindow = unit >= applier.SamplerStateStart &&
|
||||
unit - applier.SamplerStateStart < applier.SamplerStateCount;
|
||||
// The twin AT THE CSO HANDLE, asked of the same table Espryt asks (FindByHandle): a null
|
||||
// here with a live handle is the F-4 shape - a content-addressed handle looked up in a
|
||||
// table that only ever held identity-minted slots.
|
||||
if (!MGP::MGPipeHandleIsNull(cso)) {
|
||||
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.FindByHandle(cso); slot && *slot) {
|
||||
out->CsoTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||
}
|
||||
}
|
||||
|
||||
// And the twin keyed on the frontend OBJECT, which is what the pre-handle program pass
|
||||
// used to mint and bind, so a scenario can say which of the two the driver holds.
|
||||
const auto& object = MobileGL::MG_State::pGLContext->GetSamplerObject(
|
||||
static_cast<MobileGL::Uint>(glSamplerName));
|
||||
if (object) {
|
||||
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.Find(object.get()); slot && *slot) {
|
||||
out->IdentityTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool*) { return false; }
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek*) { return false; }
|
||||
bool PeekEsprytUnitSampler(unsigned, unsigned, EsprytUnitSamplerPeek*) { return false; }
|
||||
#endif
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h
|
||||
// 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
|
||||
//
|
||||
// The three readings P4aSeamAuditScenario.cpp takes from the inside, for the two seams the fable
|
||||
// seam audit proved that PUBLIC GL CANNOT SEE: F-4 (the record arm's sampler bind is a permanent
|
||||
// no-op, hidden by the pre-handle program pass binding the same values) and F-2 / SD-4 (the
|
||||
// shader-image window does not follow a program switch, hidden by the server's window/high-water
|
||||
// union taking the pre-handle bind for the units outside it). Both are correct pictures over a
|
||||
// permanent silent fallback, which is precisely the class ROADMAP.md:20 says a gate has to be
|
||||
// able to make red - and the only place the difference exists is inside.
|
||||
//
|
||||
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek.h's reason, verbatim: this file includes
|
||||
// Espryt's own Managers.h, which may not meet a scenario's GL headers in one file. It is NOT
|
||||
// PipeApplyPeek.cpp because that file is package F's (gates v3) and this round may not edit it.
|
||||
//
|
||||
// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK - a pull build, Android,
|
||||
// a backend that is not Espryt - and a caller that gets false has learned NOTHING: "could not
|
||||
// look" is not "was bound". The scenario declines the reading BY NAME and keeps its public-GL
|
||||
// half, which is the shape TextureParamsWithoutASamplerViewScenario.cpp argues for.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// ---- is Espryt's sampler family on its HANDLE arm in this process? -------------------
|
||||
//
|
||||
// The gate for every other reading here. True only on DirectGLES, in a push build, with
|
||||
// Espryt's own resolver answering "handle" for kMGPipeSubsystemSamplers (bit 11 set and its
|
||||
// dependency satisfied) - i.e. exactly when bind_sampler_states / set_shader_images are
|
||||
// consumed, so a white-box assertion about them can be red for its own reason and for no
|
||||
// other. Written only on true.
|
||||
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive);
|
||||
|
||||
// ---- the applier's shader-image window, as last received ------------------------------
|
||||
//
|
||||
// MGPipeApplierState::ShaderImageStart / ShaderImageCount / ShaderImagesSerial. Count is
|
||||
// "how many units set_shader_images last described" - 0 means the set has NEVER arrived
|
||||
// (MGPipeApplierReset advances the serial whether or not anything was emitted, so the serial
|
||||
// is not that test). Push build only.
|
||||
struct PipeShaderImageWindowPeek {
|
||||
unsigned Start;
|
||||
unsigned Count;
|
||||
unsigned long long Serial;
|
||||
};
|
||||
|
||||
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out);
|
||||
|
||||
// ---- which driver sampler a texture unit is bound to, and whose twin it is -------------
|
||||
//
|
||||
// For F-4. `BoundSamplerId` is the ES sampler name Espryt's own binding shadow says unit
|
||||
// `unit` carries (0 = none). `CsoHandleSlot/Gen` is bind_sampler_states' handle for the unit,
|
||||
// `CsoTwinSamplerId` the ES name of the twin Espryt holds AT THAT HANDLE (0 = no twin at the
|
||||
// content-addressed slot - the F-4 shape), and `IdentityTwinSamplerId` the ES name of a twin
|
||||
// keyed on the frontend SamplerObject named `glSamplerName` (0 = none). On a correct handle
|
||||
// arm the unit's driver sampler IS the CSO twin. Push build, DirectGLES only.
|
||||
struct EsprytUnitSamplerPeek {
|
||||
unsigned BoundSamplerId;
|
||||
unsigned CsoHandleSlot;
|
||||
unsigned CsoHandleGen;
|
||||
bool UnitInsideWindow;
|
||||
unsigned CsoTwinSamplerId;
|
||||
unsigned IdentityTwinSamplerId;
|
||||
};
|
||||
|
||||
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,475 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.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
|
||||
//
|
||||
// Scenario - THE FOUR SEAMS THE P4a FABLE SEAM AUDIT PROVED, each pinned by the public-GL sequence
|
||||
// (or the white-box reading) that was red on the tree the audit read and is green with its fix.
|
||||
//
|
||||
// The audit's rule, which every case here is an instance of: EVERY FIELD OF EVERY EMITTED RECORD
|
||||
// NAMES THE FRONTEND SETTER THAT CHANGES IT, AND THAT SETTER MOVES A COUNTER THE EMITTING BIT'S
|
||||
// SHUTTER READS. A record whose field has a setter no shutter sees is a stale record with nothing
|
||||
// to refuse - no census line, no Fatal, a wrong picture or a permanent silent fallback - which is
|
||||
// why none of the 80 scenarios before this file caught any of the four (Tracker.h carries the
|
||||
// record-field -> setter -> shutter table this file is the gate for).
|
||||
//
|
||||
// F-3 set_framebuffer_state INLINES an attachment's format (D-C1) and a storage redefinition of
|
||||
// an ATTACHED texture or renderbuffer moved nothing bit 11 read: Espryt's handle arm then
|
||||
// answered its alpha-widening / snorm-clamp / integer masks from the stale copy while the
|
||||
// legacy arm re-read the frontend. Three cases, both directions, texture and renderbuffer.
|
||||
// DirectGLES only: the masks are Espryt's substitution machinery.
|
||||
// F-1 set_sampler_views is resolved for the PROGRAM IN USE and bit 12's shutter read no program
|
||||
// input, so a glUseProgram alone never re-emitted it; E's record epoch (the two set serials)
|
||||
// then kept the program-independent texture sync list from ever rebuilding, and a texture
|
||||
// bound to an EMPTY slot under one program was never synced for the next. One case, both
|
||||
// backends, red as a black quad.
|
||||
// F-2 bit 14's plain-program arm mixed a per-program COUNTER two programs routinely share, so a
|
||||
// program switch never re-emitted set_shader_images and the window stayed the previous
|
||||
// program's - and E's SD-4 (a buffer image never reaching the record at all) is the same
|
||||
// bit through the null -> program transition. One case, white-box, both backends run it.
|
||||
// F-4 BindCurrentUnitSamplers' record arm looked a CONTENT-addressed CSO handle up in the
|
||||
// IDENTITY-keyed twin registry: a miss on every draw, hidden because the pre-handle program
|
||||
// pass bound the same values. One case, white-box: the unit's driver sampler must be the
|
||||
// CSO's own twin.
|
||||
//
|
||||
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
|
||||
// public-GL half (the shape TextureParamsWithoutASamplerViewScenario.cpp argues for): a pull
|
||||
// build, Magma, or a lane whose mask leaves Espryt's sampler family on its legacy arm has no
|
||||
// record arm to assert about, and skipping the whole case there would delete the public-GL
|
||||
// verdict those lanes carry. Every decline is printed and RecordProperty'd.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/P4aSeamPeek.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 {
|
||||
|
||||
constexpr int kSize = 16;
|
||||
constexpr int kInset = 2;
|
||||
|
||||
// No attributes: the quad's corners come from gl_VertexID, so a bare VAO is all a draw
|
||||
// needs and no vertex-input state can enter any of the sequences below.
|
||||
constexpr const char* kQuadVS = R"(#version 330 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kColorFS = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = uColor; }
|
||||
)";
|
||||
|
||||
// texelFetch, so WHICH image the unit holds is the whole answer and no filter, wrap or
|
||||
// completeness rule can explain a colour away.
|
||||
constexpr const char* kFetchFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
|
||||
)";
|
||||
|
||||
// texture() at (1.5, 1.5): outside the image on both axes, so the WRAP mode of whichever
|
||||
// sampler applies - the unit's sampler object or the texture's built-in one - decides
|
||||
// whether the texel or the border colour comes back.
|
||||
constexpr const char* kOutsideSampleFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 oColor;
|
||||
void main() { oColor = texture(uTex, vec2(1.5, 1.5)); }
|
||||
)";
|
||||
|
||||
// F-2 / SD-4: two compute programs over BUFFER images (the SD-4 shape - the kind E's I2
|
||||
// flip found never reached the record at all), the second naming one unit more than the
|
||||
// first, and both image-unit counters equal (layout(binding) assigns the unit at link, so
|
||||
// neither program ever moves it through glUniform1i).
|
||||
constexpr const char* kOneBufferImageCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, r32ui) writeonly uniform uimageBuffer i0;
|
||||
void main() { imageStore(i0, 0, uvec4(7u, 0u, 0u, 0u)); }
|
||||
)";
|
||||
|
||||
constexpr const char* kTwoBufferImagesCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, r32ui) readonly uniform uimageBuffer i0;
|
||||
layout(binding = 1, r32ui) writeonly uniform uimageBuffer i1;
|
||||
void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); }
|
||||
)";
|
||||
|
||||
class P4aSeamAuditScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// The F-3 cases are about Espryt's four cross-object masks, which are its own
|
||||
// substitution machinery (three-channel widening, SNORM/UNORM clamp, integer outputs);
|
||||
// Magma answers the same GL questions on its own terms, so a verdict there would pin
|
||||
// a coincidence - the same reason SnormAttachment and ThreeChannelAttachment skip.
|
||||
// Marks the case skipped; the caller tests IsSkipped() and returns (GTEST_SKIP is a
|
||||
// void statement, so it cannot return the verdict itself).
|
||||
void SkipUnlessEspryt(const char* what) {
|
||||
if (Gl().BackendName() == "DirectGLES") return;
|
||||
GTEST_SKIP() << what << " is a DirectGLES handle-arm seam; backend is " << Gl().BackendName();
|
||||
}
|
||||
|
||||
static void DrawQuad() { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
|
||||
|
||||
// One pixel's RGBA as floats, from the currently bound READ framebuffer.
|
||||
static void ReadPixelFloat(int x, int y, float out[4]) {
|
||||
out[0] = out[1] = out[2] = out[3] = -1.0f;
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_FLOAT, out);
|
||||
}
|
||||
|
||||
// The white-box gate shared by F-2 and F-4: true when Espryt's sampler family is on
|
||||
// its handle arm in this process, so the applier's unit sets are consumed and an
|
||||
// assertion about them can only be red for its own reason. Prints the decline.
|
||||
bool SamplerHandleArmIsLive(const char* what) {
|
||||
bool live = false;
|
||||
std::string why;
|
||||
if (!PeekEsprytSamplerHandleArmIsLive(&live)) {
|
||||
why = "the reading cannot be taken here (a pull build, Android, or a backend that "
|
||||
"is not DirectGLES)";
|
||||
} else if (!live) {
|
||||
why = "Espryt's sampler family runs its legacy arm in this process "
|
||||
"(MOBILEGL_PIPE_PUSH leaves bit 11 clear or refuses it)";
|
||||
}
|
||||
if (why.empty()) return true;
|
||||
std::cout << "[ P4aSeamAudit ] white-box reading DECLINED for " << what << ": " << why
|
||||
<< "; the public-GL half of the case still runs" << std::endl;
|
||||
RecordProperty("p4a_seam_white_box", "declined");
|
||||
RecordProperty("p4a_seam_white_box_reason", why);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A 2x2 RGBA8 texture filled with one colour, NEAREST, single level - complete under
|
||||
// every rule, so nothing about completeness can enter the F-1 and F-4 sequences.
|
||||
static GLuint MakeSolidTexture2D(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
std::uint8_t texels[2 * 2 * 4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
texels[i * 4 + 0] = r;
|
||||
texels[i * 4 + 1] = g;
|
||||
texels[i * 4 + 2] = b;
|
||||
texels[i * 4 + 3] = 255;
|
||||
}
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool ComputeImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
GLint maxBufferSize = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxBufferSize);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits >= 2 && maxComputeImageUniforms >= 2 && maxBufferSize >= 4;
|
||||
}
|
||||
|
||||
static GLuint MakeComputeProgram(const char* source, std::string* outError) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
*outError = 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 = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
*outError = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// An R32UI buffer texture over a fresh 4-texel buffer, every texel `fill`.
|
||||
static GLuint MakeBufferTexture(GLuint* outBuffer, GLuint fill) {
|
||||
const GLuint texels[4] = {fill, fill, fill, fill};
|
||||
glGenBuffers(1, outBuffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, *outBuffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, sizeof(texels), texels, GL_DYNAMIC_COPY);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, *outBuffer);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLuint ReadBufferTexel0(GLuint buffer) {
|
||||
GLuint value = 0xFFFFFFFFu;
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, sizeof(value), &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// F-3: a storage redefinition WHILE ATTACHED reaches the framebuffer record
|
||||
// -----------------------------------------------------------------------------------
|
||||
//
|
||||
// GL_SRGB8 is a format Espryt can only render into through its three-channel widening
|
||||
// (llvmpipe reports INCOMPLETE_ATTACHMENT for it natively - ThreeChannelAttachmentScenario
|
||||
// measured the table), so its draw buffer carries the alpha-widened mask: every draw has its
|
||||
// alpha masked off so the stored alpha stays at the 1.0 a three-channel format implies.
|
||||
// Redefine the same attached texture as GL_SRGB8_ALPHA8 and the application owns alpha
|
||||
// again - the mask must clear. On the tree the audit read the record still said SRGB8, the
|
||||
// handle arm kept masking, and the 0.25 this case draws never reached the storage.
|
||||
|
||||
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 texture was refused";
|
||||
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "an SRGB8 colour attachment must be complete (natively or through the widening)";
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
|
||||
// Phase 1: the three-channel format. Whatever the draw writes, GL reports alpha 1.0.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||
|
||||
// Phase 2: THE RESPECIFY, while attached, with no re-attach and no rebind of the FBO.
|
||||
// The only thing that moves between the two draws is the texture's storage.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8_ALPHA8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||
<< "the draw's alpha never reached a four-channel attachment: the framebuffer record "
|
||||
"still describes the three-channel storage the texture was attached with, so the "
|
||||
"handle arm kept masking alpha off (F-3)";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The renderbuffer twin. A renderbuffer's three storage setters bump no version at all:
|
||||
// D-D2 closed the RESOURCE record by emitting from the entry point and left the framebuffer
|
||||
// record - and with it the masks - describing the storage it was attached with.
|
||||
TEST_F(P4aSeamAuditScenario, ARenderbufferRestoragedWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
glGenRenderbuffers(1, &renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8, kSize, kSize);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 renderbuffer was refused";
|
||||
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "an SRGB8 renderbuffer attachment must be complete (natively or through the widening)";
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||
|
||||
// THE RE-STORAGE, while attached.
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8_ALPHA8, kSize, kSize);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the re-storage to SRGB8_ALPHA8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
DrawQuad();
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||
<< "the draw's alpha never reached the four-channel renderbuffer: the framebuffer "
|
||||
"record still describes the storage it was attached with (F-3)";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &renderbuffer);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The mirror direction, four channels -> three, and it needs the driver to READ the stored
|
||||
// alpha because the readback fixup (which consults the frontend) would hide it: after the
|
||||
// respecify to SRGB8 the widening discipline has to hold - the clear puts 1.0 into the
|
||||
// carrier's alpha and the draw is masked away from it - so a GL_DST_ALPHA blend of white sees
|
||||
// 1.0. On a stale record the draw wrote its 0.25 into the carrier and the blend saw that.
|
||||
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedToThreeChannelsWhileAttachedReachesTheFramebufferRecord) {
|
||||
if (!Ready()) return;
|
||||
SkipUnlessEspryt("F-3");
|
||||
if (IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||
ASSERT_GE(colorLocation, 0);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8_ALPHA8 texture was refused";
|
||||
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glUseProgram(program);
|
||||
|
||||
// Phase 1: four channels, the application owns alpha.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
DrawQuad();
|
||||
float pixel[4];
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[3], 0.25f, 0.02f) << "a four-channel attachment stores the draw's alpha";
|
||||
|
||||
// Phase 2: THE RESPECIFY to three channels, while attached.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8 was refused";
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||
DrawQuad();
|
||||
// dst = stored alpha; src factor GL_DST_ALPHA, dst factor GL_ZERO, source white =>
|
||||
// the colour becomes (storedAlpha, storedAlpha, storedAlpha) - ThreeChannelAttachment's
|
||||
// own probe, which nothing on the readback path can doctor.
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_DST_ALPHA, GL_ZERO);
|
||||
glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawQuad();
|
||||
glDisable(GL_BLEND);
|
||||
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||
EXPECT_NEAR(pixel[0], 1.0f, 0.05f)
|
||||
<< "GL_DST_ALPHA read the stored alpha of a three-channel attachment and it was not "
|
||||
"1.0: the framebuffer record still describes the four-channel storage the texture "
|
||||
"was attached with, so the handle arm let the draw write alpha (F-3, mirror)";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -56,7 +56,11 @@ namespace MobileGL::MG_Pipe {
|
||||
enum class MGPipeAggregate : Uint32 {
|
||||
// VertexArrayState: any VAO attribute format / buffer / enable moved.
|
||||
VaoAttribute = 0,
|
||||
// FramebufferState: any FBO attachment or default-geometry write, or a bind.
|
||||
// FramebufferState: any FBO attachment or default-geometry write, or a bind - and, since
|
||||
// P4a (fable seam F-3), any STORAGE DEFINITION of a texture or a renderbuffer, because
|
||||
// set_framebuffer_state inlines an attachment's format, extent and samples and those
|
||||
// setters are the only writers of what it inlines (TextureObject.cpp /
|
||||
// RenderbufferObject.cpp, PipePublishDescriptor).
|
||||
FramebufferAttachment,
|
||||
// TextureState: any texture object CONTENT moved (an upload, a dirty region).
|
||||
TextureContent,
|
||||
|
||||
@@ -137,18 +137,28 @@ namespace MobileGL {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// D-D2: THE RENDERBUFFER PUBLICATION HOLE, CLOSED BY EMISSION AND NOT BY A NEW
|
||||
// VERSION. These three setters bump no version and raise no notice, and the
|
||||
// framebuffer dirty bit's shutter does not move when an ALREADY-ATTACHED renderbuffer
|
||||
// is re-storaged - so `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an
|
||||
// attached renderbuffer was invisible to everything downstream. Emitting from the
|
||||
// storage entry point closes it; a version counter here would resize the pull build's
|
||||
// object and break G1, and widening the shutter would fire the framebuffer emission on
|
||||
// an unrelated renderbuffer write.
|
||||
// VERSION. These three setters bump no version and raise no notice, so
|
||||
// `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an attached renderbuffer
|
||||
// was invisible to everything downstream. Emitting from the storage entry point
|
||||
// closes the RESOURCE half; a version counter here would resize the pull build's
|
||||
// object and break G1.
|
||||
//
|
||||
// THE FRAMEBUFFER HALF IS THE AGGREGATE BUMP BELOW (P4a fable seam F-3), and the
|
||||
// sentence that used to end the paragraph above - "widening the shutter would fire
|
||||
// the framebuffer emission on an unrelated renderbuffer write" - was the seam:
|
||||
// set_framebuffer_state inlines an attachment's InternalFormat, extent and Samples at
|
||||
// emission (D-C1), so re-storaging an ATTACHED renderbuffer left the framebuffer
|
||||
// record - and the handle arm's four cross-object masks - describing the previous
|
||||
// storage while the resource record described the new one. The bump costs one
|
||||
// framebuffer re-emission per storage definition, whether or not the object is
|
||||
// attached, which the emitter's content hash suppresses when nothing it inlines
|
||||
// moved; it is not a counter on this object.
|
||||
//
|
||||
// The emitter dedupes on the built descriptor, so glRenderbufferStorage's three-setter
|
||||
// sequence publishes once rather than three times.
|
||||
void RenderbufferObject::PipePublishDescriptor() {
|
||||
MG_Pipe::MGPipeEmitRenderbufferResourceRespecify(*this);
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
#endif
|
||||
} // namespace GLState
|
||||
|
||||
@@ -67,6 +67,20 @@ namespace MobileGL {
|
||||
|
||||
void TextureObjectBase::PipePublishDescriptor() {
|
||||
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this);
|
||||
// AND THE FRAMEBUFFER AGGREGATE MOVES (P4a fable seam F-3). The resource record
|
||||
// above is only half of what a storage definition changes: set_framebuffer_state
|
||||
// INLINES an attachment's InternalFormat, TextureTarget, extent, Samples and
|
||||
// Complete at emission (D-C1), so redefining the storage of a texture that is
|
||||
// ATTACHED changed those fields with nothing bit 11 reads moving - the format
|
||||
// and shape setters bump the two TEXTURE aggregates and never the attachment
|
||||
// one, and no path from a texture reaches its framebuffers. The handle arm then
|
||||
// answered its four cross-object masks from the stale copy while the legacy arm
|
||||
// re-read the frontend at the same re-sync. This is the one funnel every
|
||||
// storage-defining entry point takes (see AllocateStorage), so the bump lives
|
||||
// here and not per setter, it is push-only like the rest of this block, and it
|
||||
// over-fires the framebuffer bit once per storage definition of an unattached
|
||||
// texture - at load time, where a 304-byte hash is nothing.
|
||||
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||
}
|
||||
|
||||
void TextureObjectBase::PipePublishParams() {
|
||||
|
||||
@@ -80,6 +80,8 @@ namespace {
|
||||
X(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) \
|
||||
X(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) \
|
||||
X(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) \
|
||||
X(TrackerAggregates, ATextureStorageDefinitionMovesTheFramebufferAggregateToo) \
|
||||
X(TrackerAggregates, ARenderbufferStorageDefinitionMovesTheFramebufferAggregate) \
|
||||
X(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AnIntWriteCarriesTheIntWordsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AUintWriteCarriesTheUintWordsAndNamesItsClass) \
|
||||
@@ -232,6 +234,39 @@ namespace {
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
// P4a FABLE SEAM F-3. set_framebuffer_state INLINES an attachment's format, extent and
|
||||
// samples (D-C1), so the setters that define a texture's storage are setters of a
|
||||
// framebuffer-record field - and the record-field -> setter -> shutter rule (Tracker.h's
|
||||
// table) says they must move the aggregate bit 11 reads. They still move the params
|
||||
// aggregate they always moved; what this case pins is the SECOND bump, which the
|
||||
// "moves only" cases above cannot see and which is the whole of F-3's fix.
|
||||
TEST_F(TrackerAggregates, ATextureStorageDefinitionMovesTheFramebufferAggregateToo) {
|
||||
const auto& tex = Ctx().CreateTextureObject(3, TextureTarget::Texture2D);
|
||||
ASSERT_TRUE(tex != nullptr);
|
||||
const Snapshot before = Snap();
|
||||
tex->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8);
|
||||
const Snapshot after = Snap();
|
||||
EXPECT_GT(after[MGPipeAggregate::FramebufferAttachment], before[MGPipeAggregate::FramebufferAttachment])
|
||||
<< "a texture whose storage is redefined WHILE ATTACHED left set_framebuffer_state "
|
||||
"describing the previous format (F-3)";
|
||||
EXPECT_GT(after[MGPipeAggregate::TextureParams], before[MGPipeAggregate::TextureParams]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::TextureContent], before[MGPipeAggregate::TextureContent]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::VaoAttribute], before[MGPipeAggregate::VaoAttribute]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::BufferChange], before[MGPipeAggregate::BufferChange]);
|
||||
EXPECT_EQ(after[MGPipeAggregate::VertexAttribDefault], before[MGPipeAggregate::VertexAttribDefault]);
|
||||
}
|
||||
|
||||
// The renderbuffer twin, and the one that had NO aggregate at all before: its three storage
|
||||
// setters bumped no version and raised no notice (D-D2 closed the resource record by emitting
|
||||
// from the entry point and left the framebuffer record stale).
|
||||
TEST_F(TrackerAggregates, ARenderbufferStorageDefinitionMovesTheFramebufferAggregate) {
|
||||
const auto& rbo = Ctx().CreateRenderbufferObject(1);
|
||||
ASSERT_TRUE(rbo != nullptr);
|
||||
const Snapshot before = Snap();
|
||||
rbo->AllocateStorage(IntVec2{8, 8});
|
||||
ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap());
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// The dirty walk itself, and the render-state emission it drives (P2 brief D4, D6, D7)
|
||||
// ===================================================================================
|
||||
|
||||
Reference in New Issue
Block a user