[MG_IntegrationTest] (Disaggregated): prove real Magma queued state and present credit

This commit is contained in:
2026-09-20 04:02:44 -04:00
parent 0e4afb624f
commit c1960fdfaa
7 changed files with 486 additions and 14 deletions
+25
View File
@@ -1474,6 +1474,31 @@ jobs:
["DirectVulkan.Split.Buffers." + name for name in cases])
PY
- name: Magma run-ahead must queue real GPU work and enforce credits 1 and 3
working-directory: build-split
run: |
ctest -L integration-magma-runahead --show-only=json-v1 > "${RUNNER_TEMP}/magma-runahead-lane.json"
ctest -L integration-magma-runahead --no-tests=error -j 2 --output-on-failure \
--output-junit "${RUNNER_TEMP}/magma-runahead.xml"
python3 - "${GITHUB_WORKSPACE}/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py" \
"${RUNNER_TEMP}/magma-runahead-lane.json" "${RUNNER_TEMP}/magma-runahead.xml" <<'PY'
import importlib.util, json, sys
spec = importlib.util.spec_from_file_location("slp", sys.argv[1])
slp = importlib.util.module_from_spec(spec); spec.loader.exec_module(slp)
cases = [
"QueuedBufferVersionsAndDeletedNameReuseKeepTheirPixels",
"QueuedProgramRebindsKeepEachUniformSnapshot",
"QueuedGpuWritesReadBackTheLastDispatchBytes",
"MixedNativeAndConvertedGpuVertexStreamsKeepTheirReservation",
"TextureUploadCannotOvertakeThePreviouslyQueuedDraw",
"GpuClearSurvivesTheFirstStorageImageUpgrade",
"PresentCreditActuallyParksTheClientAtItsConfiguredLimit",
]
required = [f"DirectVulkan.Split.RunAhead.Credit{credit}.MagmaRunAheadScenario.{name}"
for credit in (1, 3) for name in cases]
slp.require_green(json.load(open(sys.argv[2])), sys.argv[3], required)
PY
- name: Magma P7 native mip format debt stays a named refusal
working-directory: build-split
env:
@@ -2457,6 +2457,39 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach()
endif()
# Real Magma run-ahead, including client lead and present-credit backpressure.
# The test fixture requires an actually armed production session; a missing cap
# fails rather than turning this dedicated lane into a skip or a lockstep pass.
if (MOBILEGL_BUILD_DISAGGREGATED)
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/MagmaRunAheadScenario.cpp)
foreach(credit 1 3)
mgl_itest_join_environment(MGL_ITEST_MAGMA_RUNAHEAD_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_TRANSPORT=inproc"
"MOBILEGL_IPC_ROLE_SPLIT_STATE=1" "MOBILEGL_IPC_STRICT_ERRORS=1"
"MOBILEGL_IPC_RUN_AHEAD=1" "MOBILEGL_IPC_PRESENT_CREDIT=${credit}"
"MOBILEGL_IPC_SPIN_US=0" "MOBILEGL_IPC_ADOPT_TIER=2"
"MGITEST_MAGMA_RUNAHEAD_LANE=1" "MGITEST_SPLIT_LANE=1"
"MOBILEGL_ITEST_REQUIRE_GPU=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
foreach(runAheadCase
QueuedBufferVersionsAndDeletedNameReuseKeepTheirPixels
QueuedProgramRebindsKeepEachUniformSnapshot
QueuedGpuWritesReadBackTheLastDispatchBytes
MixedNativeAndConvertedGpuVertexStreamsKeepTheirReservation
TextureUploadCannotOvertakeThePreviouslyQueuedDraw
GpuClearSurvivesTheFirstStorageImageUpgrade
PresentCreditActuallyParksTheClientAtItsConfiguredLimit)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.Split.RunAhead.Credit${credit}."
TEST_FILTER "MagmaRunAheadScenario.${runAheadCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "integration-magma-split\;integration-magma-runahead"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_MAGMA_RUNAHEAD_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-runahead-credit${credit}-${runAheadCase}.log")
endforeach()
endforeach()
endif()
# P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, split-only source for the
# same reason as f1's - the scenario reads the SERVER sink's tallies, which a monolith build
# has no symbol for. One block per case with a LOG PATH OF ITS OWN, the F1 pattern:
@@ -21,11 +21,74 @@
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <chrono>
#include <atomic>
#include <thread>
#define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1
#endif
namespace MGITest {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
namespace {
std::atomic<bool> g_holdArmed{false}, g_holdActive{false}, g_holdRelease{true};
void HoldOneAppliedBatch() {
if (!g_holdArmed.exchange(false, std::memory_order_acq_rel)) return;
g_holdActive.store(true, std::memory_order_release);
const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!g_holdRelease.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < until)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
g_holdActive.store(false, std::memory_order_release);
}
}
#endif
bool ArmSplitApplyHoldForTesting() {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
g_holdRelease.store(false, std::memory_order_release);
g_holdArmed.store(true, std::memory_order_release);
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(&HoldOneAppliedBatch);
return true;
#else
return false;
#endif
}
bool WaitForSplitApplyHoldForTesting(unsigned int timeoutMs) {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
while (std::chrono::steady_clock::now() < until) {
if (g_holdActive.load(std::memory_order_acquire)) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
#else
(void)timeoutMs;
#endif
return false;
}
bool SplitApplyHoldIsActiveForTesting() {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
return g_holdActive.load(std::memory_order_acquire);
#else
return false;
#endif
}
void ReleaseSplitApplyHoldForTesting() {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
g_holdRelease.store(true, std::memory_order_release);
g_holdArmed.store(false, std::memory_order_release);
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(nullptr);
while (g_holdActive.load(std::memory_order_acquire))
std::this_thread::sleep_for(std::chrono::milliseconds(1));
#endif
}
bool SplitProducerIsParkedForTesting() {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
// This is the only reader intended for the observer thread. Do not call
// PeekSplitRuntime there: its encoder and client-ledger fields are not atomic.
auto* session = MobileGL::MG_Remote::Client::ClientSession::Active();
return session && session->Control() && session->Control()->producerParked.load(std::memory_order_acquire);
#else
return false;
#endif
}
void DelaySplitRetirementForTesting(bool enabled) {
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(
@@ -60,6 +123,15 @@ namespace MGITest {
// neither emits anything - this is a read.
const MobileGL::MG_Remote::Wire::PipeWireEncoder& encoder = session->Encoder();
state.emitSeq = encoder.EmitSeq();
state.runAheadArmed = session->RunAheadArmed();
state.presentCredit = MobileGL::MG_Config::Ipc.PresentCredit;
state.presentCreditWaits = session->PresentCreditWaits();
if (const auto* control = session->Control()) {
state.appliedSeq = control->appliedSeq.load(std::memory_order_acquire);
state.retiredSeq = control->retiredSeq.load(std::memory_order_acquire);
state.presentAckSerial = control->presentAckSerial.load(std::memory_order_acquire);
}
// The producer's ledger. Every one of these is a plain member read on the encoder
// or on the RingProducer it holds; none of them emits, publishes or waits, so a
// case may read them between two GL calls without changing what the next record is.
@@ -70,6 +70,15 @@ namespace MGITest {
unsigned int totalVerbSlots = 0;
// The encoder's highest produced record ordinal, or 0 when there is no session.
unsigned long long emitSeq = 0;
// Real session state/watermarks. Only the client thread reads this snapshot;
// the scheduling observer below reads producerParked separately.
bool runAheadArmed = false;
unsigned int presentCredit = 0;
unsigned long long appliedSeq = 0;
unsigned long long retiredSeq = 0;
unsigned long long presentAckSerial = 0;
unsigned long long presentCreditWaits = 0;
// ---- the wire producer's ledger, for exit gates E3(e) and R-10's proof obligation ---
//
@@ -104,6 +113,15 @@ namespace MGITest {
SplitRuntimeState PeekSplitRuntime();
// Scheduling-only perturbation; never changes a watermark or counter.
void DelaySplitRetirementForTesting(bool enabled);
// Holds the existing before-retire scheduling hook once, after a real drain
// batch. Never writes sequence numbers, caps, pixel state or production data.
// A two-second fail-safe releases a lockstep negative control without hanging.
bool ArmSplitApplyHoldForTesting();
bool WaitForSplitApplyHoldForTesting(unsigned int timeoutMs = 1000);
bool SplitApplyHoldIsActiveForTesting();
void ReleaseSplitApplyHoldForTesting();
bool SplitProducerIsParkedForTesting();
// Empty when this process is a real split run that can be asserted about; otherwise the
// reason to GTEST_SKIP() with, naming the first fact that is not true and the package that
@@ -0,0 +1,328 @@
// Real Magma run-ahead: observe client lead over an actually held apply thread,
// then verify GPU pixels/bytes. The scheduling hook never changes a watermark.
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstring>
#include <initializer_list>
#include <thread>
#include <utility>
#include <vector>
#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 kWidth = 32, kHeight = 8;
constexpr const char* kVertexId = R"(#version 430 core
void main() {
vec2 p[3] = vec2[3](vec2(-1,-1),vec2(3,-1),vec2(-1,3));
gl_Position=vec4(p[gl_VertexID],0,1);
})";
using Pixel = std::array<GLubyte, 4>;
constexpr Pixel red{255,0,0,255}, green{0,255,0,255}, blue{0,0,255,255}, yellow{255,255,0,255}, black{0,0,0,255};
struct HeldApply {
~HeldApply() { ReleaseSplitApplyHoldForTesting(); }
void Release() { ReleaseSplitApplyHoldForTesting(); }
};
class MagmaRunAheadScenario : public ScenarioTest {
protected:
GLuint target = 0;
std::vector<GLuint> buffers, programs, textures, fbos, vaos;
SplitRuntimeState held{};
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!SplitLane::MarkerIsOne("MGITEST_MAGMA_RUNAHEAD_LANE"))
GTEST_SKIP() << "dedicated Magma run-ahead lane only";
ASSERT_EQ(Gl().BackendName(), "DirectVulkan");
const auto runtime = PeekSplitRuntime();
ASSERT_TRUE(runtime.peekAvailable && runtime.sessionActive && runtime.transportResolved);
ASSERT_TRUE(runtime.runAheadArmed) << "Magma production session did not arm run-ahead";
ASSERT_TRUE(runtime.presentCredit == 1 || runtime.presentCredit == 3);
target = NewFbo(NewTexture(kWidth, kHeight, nullptr));
glBindFramebuffer(GL_FRAMEBUFFER, target);
glViewport(0, 0, kWidth, kHeight);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
vaos.push_back(vao);
glBindVertexArray(vao);
}
void TearDown() override {
ReleaseSplitApplyHoldForTesting();
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (!buffers.empty()) glDeleteBuffers(GLsizei(buffers.size()), buffers.data());
if (!vaos.empty()) glDeleteVertexArrays(GLsizei(vaos.size()), vaos.data());
if (!fbos.empty()) glDeleteFramebuffers(GLsizei(fbos.size()), fbos.data());
if (!textures.empty()) glDeleteTextures(GLsizei(textures.size()), textures.data());
for (GLuint program : programs) glDeleteProgram(program);
}
GLuint NewBuffer(GLenum target, GLsizeiptr size, const void* bytes) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
buffers.push_back(buffer);
glBindBuffer(target, buffer);
glBufferData(target, size, bytes, GL_DYNAMIC_DRAW);
return buffer;
}
GLuint NewTexture(int width, int height, const void* pixels) {
GLuint texture = 0;
glGenTextures(1, &texture);
textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixels);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
return texture;
}
GLuint NewFbo(GLuint texture) {
GLuint fbo = 0;
glGenFramebuffers(1,&fbo);
fbos.push_back(fbo);
glBindFramebuffer(GL_FRAMEBUFFER,fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
return fbo;
}
GLuint Link(std::initializer_list<std::pair<GLenum,const char*>> stages) {
const GLuint program = glCreateProgram();
programs.push_back(program);
for (const auto& [kind, source] : stages) {
const GLuint shader = glCreateShader(kind);
glShaderSource(shader,1,&source,nullptr);
glCompileShader(shader);
GLint ok = GL_FALSE;
glGetShaderiv(shader,GL_COMPILE_STATUS,&ok);
EXPECT_EQ(ok,GL_TRUE) << "run-ahead shader compile";
glAttachShader(program,shader);
glDeleteShader(shader);
}
glLinkProgram(program);
GLint ok = GL_FALSE;
glGetProgramiv(program,GL_LINK_STATUS,&ok);
EXPECT_EQ(ok,GL_TRUE) << "run-ahead program link";
return program;
}
void HoldNextBatch() {
// Fully drain SETUP, then hold after one real clear batch. No wait/readback
// is inserted among the operations whose queued ordering is under test.
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
ASSERT_TRUE(ArmSplitApplyHoldForTesting());
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_TRUE(WaitForSplitApplyHoldForTesting()) << "server did not enter the scheduling hold";
held = PeekSplitRuntime();
}
void ExpectQueued(unsigned int minimumRecords) {
const auto now = PeekSplitRuntime();
EXPECT_TRUE(SplitApplyHoldIsActiveForTesting()) << "client waited for apply instead of running ahead";
EXPECT_EQ(now.appliedSeq, held.appliedSeq) << "held server advanced unexpectedly";
EXPECT_GE(now.emitSeq, held.emitSeq + minimumRecords);
EXPECT_GT(now.emitSeq, now.appliedSeq) << "no real client lead over the apply watermark";
RecordProperty("observed_queued_records", std::to_string(now.emitSeq - now.appliedSeq));
}
void DrawTile(int tile) {
glViewport(tile * 8,0,8,kHeight);
glDrawArrays(GL_TRIANGLES,0,3);
}
void ExpectPixel(int x, int y, Pixel expected, const char* why) {
Pixel actual{};
glReadPixels(x,y,1,1,GL_RGBA,GL_UNSIGNED_BYTE,actual.data());
EXPECT_EQ(actual,expected) << why;
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << why;
}
};
TEST_F(MagmaRunAheadScenario, QueuedBufferVersionsAndDeletedNameReuseKeepTheirPixels) {
if (!Ready() || IsSkipped()) return;
const char* vs = R"(#version 430 core
layout(location=0) in vec4 color; out vec4 v;
void main(){vec2 p[3]=vec2[3](vec2(-1,-1),vec2(3,-1),vec2(-1,3));gl_Position=vec4(p[gl_VertexID],0,1);v=color;})";
const char* fs = "#version 430 core\nin vec4 v;layout(location=0) out vec4 c;void main(){c=v;}";
glUseProgram(Link({{GL_VERTEX_SHADER,vs},{GL_FRAGMENT_SHADER,fs}}));
const auto colors = [](float r,float g,float b) { return std::array<GLfloat,12>{r,g,b,1,r,g,b,1,r,g,b,1}; };
const auto r=colors(1,0,0), g=colors(0,1,0), b=colors(0,0,1), y=colors(1,1,0);
const GLuint buffer=NewBuffer(GL_ARRAY_BUFFER,sizeof(r),r.data());
glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,16,nullptr); glEnableVertexAttribArray(0);
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
DrawTile(0);
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(g),g.data()); DrawTile(1);
glBufferData(GL_ARRAY_BUFFER,sizeof(b),b.data(),GL_STREAM_DRAW); DrawTile(2);
glDeleteBuffers(1,&buffer);
// Request the recycled name through the public allocator. Record reuse must
// have a distinct lifetime even though the application-visible name is equal.
GLuint replacement=0;
glGenBuffers(1,&replacement);
EXPECT_EQ(replacement,buffer) << "the intended GL-name reuse did not occur";
if (replacement!=buffer) buffers.push_back(replacement);
glBindBuffer(GL_ARRAY_BUFFER,replacement);
glBufferData(GL_ARRAY_BUFFER,sizeof(y),y.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,16,nullptr); DrawTile(3);
ExpectQueued(4); hold.Release();
ExpectPixel(4,4,red,"original buffer version"); ExpectPixel(12,4,green,"SubData version");
ExpectPixel(20,4,blue,"respecified store"); ExpectPixel(28,4,yellow,"recreated GL name");
}
TEST_F(MagmaRunAheadScenario, QueuedProgramRebindsKeepEachUniformSnapshot) {
if (!Ready() || IsSkipped()) return;
const char* a="#version 430 core\nuniform vec4 tint;layout(location=0) out vec4 c;void main(){c=tint;}";
const char* b="#version 430 core\nuniform vec4 tint;layout(location=0) out vec4 c;void main(){c=tint.bgra;}";
const GLuint pa=Link({{GL_VERTEX_SHADER,kVertexId},{GL_FRAGMENT_SHADER,a}}), pb=Link({{GL_VERTEX_SHADER,kVertexId},{GL_FRAGMENT_SHADER,b}});
const GLint la=glGetUniformLocation(pa,"tint"), lb=glGetUniformLocation(pb,"tint");
ASSERT_GE(la,0); ASSERT_GE(lb,0);
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
glUseProgram(pa); glUniform4f(la,1,0,0,1); DrawTile(0);
glUseProgram(pb); glUniform4f(lb,0,1,0,1); DrawTile(1);
glUseProgram(pa); glUniform4f(la,0,0,1,1); DrawTile(2);
glUseProgram(pb); glUniform4f(lb,0,1,1,1); DrawTile(3);
ExpectQueued(4); hold.Release();
ExpectPixel(4,4,red,"program A first uniform"); ExpectPixel(12,4,green,"program B first uniform");
ExpectPixel(20,4,blue,"program A rebound"); ExpectPixel(28,4,yellow,"program B swizzled second uniform");
}
TEST_F(MagmaRunAheadScenario, QueuedGpuWritesReadBackTheLastDispatchBytes) {
if (!Ready() || IsSkipped()) return;
const char* cs=R"(#version 430 core
layout(local_size_x=1) in;layout(std430,binding=0) buffer Out{uint value[];};uniform uint seed;
void main(){uint i=gl_GlobalInvocationID.x;value[i]=seed+i;})";
const GLuint program=Link({{GL_COMPUTE_SHADER,cs}});
const GLint seed=glGetUniformLocation(program,"seed"); ASSERT_GE(seed,0);
std::array<GLuint,32> data{};
const GLuint buffer=NewBuffer(GL_SHADER_STORAGE_BUFFER,sizeof(data),data.data());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER,0,buffer); glUseProgram(program);
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
glUniform1ui(seed,0x11110000u); glDispatchCompute(32,1,1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glUniform1ui(seed,0x22220000u); glDispatchCompute(32,1,1);
ExpectQueued(2); hold.Release();
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER,0,sizeof(data),data.data());
for (GLuint i=0;i<data.size();++i) EXPECT_EQ(data[i],0x22220000u+i) << i;
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); glBindBufferBase(GL_SHADER_STORAGE_BUFFER,0,0);
}
TEST_F(MagmaRunAheadScenario, MixedNativeAndConvertedGpuVertexStreamsKeepTheirReservation) {
if (!Ready() || IsSkipped()) return;
const char* cs=R"(#version 430 core
layout(local_size_x=1) in;layout(std430,binding=0) buffer Out{uint words[];};
void main(){vec2 p[3]=vec2[3](vec2(-1,-1),vec2(0,-1),vec2(-1,1));
for(uint i=0u;i<3u;++i){words[4u*i]=floatBitsToUint(p[i].x);words[4u*i+1u]=floatBitsToUint(p[i].y);
words[4u*i+2u]=0u;words[4u*i+3u]=0x3ff00000u;}})";
const char* vs=R"(#version 430 core
layout(location=0) in vec2 position;layout(location=1) in double code;out vec4 v;
void main(){gl_Position=vec4(position,0,1);v=vec4(1.0-float(code),float(code),0,1);})";
const char* fs="#version 430 core\nin vec4 v;layout(location=0) out vec4 c;void main(){c=v;}";
const GLuint compute=Link({{GL_COMPUTE_SHADER,cs}}), graphics=Link({{GL_VERTEX_SHADER,vs},{GL_FRAGMENT_SHADER,fs}});
struct Vertex { GLfloat x,y; GLdouble code; };
static_assert(sizeof(Vertex)==16);
std::array<Vertex,3> vertices{};
const GLuint buffer=NewBuffer(GL_ARRAY_BUFFER,sizeof(vertices),vertices.data());
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,16,nullptr); glEnableVertexAttribArray(0);
glVertexAttribLPointer(1,1,GL_DOUBLE,16,reinterpret_cast<const void*>(8)); glEnableVertexAttribArray(1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER,0,buffer);
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
glUseProgram(compute); glDispatchCompute(1,1,1);
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
glUseProgram(graphics); glDrawArrays(GL_TRIANGLES,0,3);
vertices={Vertex{0,-1,0},Vertex{1,-1,0},Vertex{1,1,0}};
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(vertices),vertices.data());
glDrawArrays(GL_TRIANGLES,0,3);
ExpectQueued(3); hold.Release();
ExpectPixel(2,1,green,"native position reservation survives the other attribute's Double conversion read");
ExpectPixel(29,1,red,"later SubData reaches only the later draw");
glBindBufferBase(GL_SHADER_STORAGE_BUFFER,0,0);
}
TEST_F(MagmaRunAheadScenario, TextureUploadCannotOvertakeThePreviouslyQueuedDraw) {
if (!Ready() || IsSkipped()) return;
const char* fs="#version 430 core\nuniform sampler2D src;layout(location=0) out vec4 c;void main(){c=texture(src,vec2(0.5));}";
glUseProgram(Link({{GL_VERTEX_SHADER,kVertexId},{GL_FRAGMENT_SHADER,fs}}));
const std::array<GLubyte,16> oldPixels{255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255};
const std::array<GLubyte,16> newPixels{0,255,0,255,0,255,0,255,0,255,0,255,0,255,0,255};
const GLuint source=NewTexture(2,2,oldPixels.data());
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
DrawTile(0); ExpectQueued(1); hold.Release();
// Texture uploads have a wire reply. That is not a GPU completion barrier for
// the prior draw; no ReadPixels/Finish is permitted before the second draw.
glBindTexture(GL_TEXTURE_2D,source);
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,2,2,GL_RGBA,GL_UNSIGNED_BYTE,newPixels.data());
DrawTile(1);
ExpectPixel(4,4,red,"draw issued before texture update");
ExpectPixel(12,4,green,"draw issued after texture update");
}
TEST_F(MagmaRunAheadScenario, GpuClearSurvivesTheFirstStorageImageUpgrade) {
if (!Ready() || IsSkipped()) return;
std::array<GLubyte,4*4*4> bytes{};
for (size_t i=0;i<bytes.size();i+=4) { bytes[i]=255; bytes[i+3]=255; }
const GLuint source=NewTexture(4,4,bytes.data()), sourceFbo=NewFbo(source);
const char* cs=R"(#version 430 core
layout(local_size_x=1) in;layout(rgba8,binding=0) uniform writeonly image2D img;
void main(){imageStore(img,ivec2(0),vec4(0,0,1,1));})";
const GLuint compute=Link({{GL_COMPUTE_SHADER,cs}});
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
glClearColor(0,1,0,1); glClear(GL_COLOR_BUFFER_BIT); ExpectQueued(1); hold.Release();
glBindFramebuffer(GL_FRAMEBUFFER,target);
glBindImageTexture(0,source,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA8);
glUseProgram(compute); glDispatchCompute(1,1,1);
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
glBindFramebuffer(GL_READ_FRAMEBUFFER,sourceFbo);
ExpectPixel(0,0,blue,"compute writes the new STORAGE image");
ExpectPixel(2,2,green,"usage upgrade preserves the earlier GPU clear, not the old red CPU upload");
glBindImageTexture(0,0,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA8);
}
TEST_F(MagmaRunAheadScenario, PresentCreditActuallyParksTheClientAtItsConfiguredLimit) {
if (!Ready() || IsSkipped()) return;
glBindFramebuffer(GL_FRAMEBUFFER,0);
HeldApply hold; ASSERT_NO_FATAL_FAILURE(HoldNextBatch());
const auto before=PeekSplitRuntime();
ASSERT_EQ(before.presentAckSerial,0u) << "this isolated case has not presented yet";
for (unsigned i=0;i<before.presentCredit;++i) { glClear(GL_COLOR_BUFFER_BIT); Gl().EndFrame(); }
ExpectQueued(before.presentCredit);
const auto full=PeekSplitRuntime();
EXPECT_EQ(full.presentAckSerial,before.presentAckSerial);
EXPECT_EQ(full.presentCreditWaits,before.presentCreditWaits);
std::atomic<bool> sawPark{false}, stop{false};
std::thread observer([&] {
const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(1);
while (!stop.load() && std::chrono::steady_clock::now()<until) {
if (SplitProducerIsParkedForTesting()) { sawPark.store(true); break; }
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
ReleaseSplitApplyHoldForTesting();
});
Gl().EndFrame(); // credit + 1 must park before it can publish its present.
stop.store(true); observer.join(); hold.Release();
EXPECT_TRUE(sawPark.load()) << "no real producer park while the present window was full";
glFinish();
const auto after=PeekSplitRuntime();
EXPECT_EQ(after.presentCreditWaits,before.presentCreditWaits+1);
EXPECT_EQ(after.presentAckSerial,before.presentCredit+1);
EXPECT_EQ(after.appliedSeq,after.emitSeq);
RecordProperty("observed_present_credit",std::to_string(before.presentCredit));
RecordProperty("observed_producer_park",sawPark.load() ? "yes" : "no");
}
} // namespace
} // namespace MGITest
@@ -218,19 +218,14 @@ namespace {
EXPECT_EQ(mint.Count(), 1u);
}
// Application buffer record consumers do not imply run-ahead readiness.
// Magma retains its lockstep lifetime/submission contract; the capability
// must remain absent even when the Espryt readiness constant is true.
// Historical test name retained. Each backend's readiness is now a real gate:
// false forbids the capability, true permits it. Production keeps its own
// Magma readiness constant, which the bootstrap and GPU queue tests also check.
TEST_F(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) {
// Whatever the integration constant says. `true` is what the P5e integration commit
// will pass, so the Magma answer is pinned on BOTH sides of that flip and the case
// does not quietly stop asserting anything the day the constant moves.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false), 0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true),
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::Unknown, /*ready=*/true), 0u);
// And the Espryt half, so the case says what the arm IS and not only what it is not:
// the bit is published for DirectGLES and ONLY once the integration commit flips
+3 -2
View File
@@ -634,6 +634,7 @@ TEST(CapsMirrorTest, ObjectFamilyEmissionTracksIndependentConsumerCapsWithoutBuf
#endif
}
// Historical name retained; the real bootstrap must now publish and arm Magma readiness.
TEST(CapsMirrorTest, MagmaTransportPublishesRealBufferConsumersWithoutRunAhead) {
#if MGTEST_HAVE_FORK
const ChildResult result = RunInChild([] {
@@ -654,7 +655,7 @@ TEST(CapsMirrorTest, MagmaTransportPublishesRealBufferConsumersWithoutRunAhead)
const Uint64 mask = server.CallMask();
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemResources));
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemBufferBindings));
EXPECT_EQ(mask & static_cast<Uint64>(kCapRunAheadApply), 0u);
EXPECT_EQ(mask & static_cast<Uint64>(kCapRunAheadApply), static_cast<Uint64>(kCapRunAheadApply));
}
const auto* ops = MG_Pipe::MGPipeGetResourceOps();
EXPECT_NE(ops, nullptr);
@@ -669,7 +670,7 @@ TEST(CapsMirrorTest, MagmaTransportPublishesRealBufferConsumersWithoutRunAhead)
if (ops->MapPersistent) EXPECT_EQ(ops->MapPersistent({7, 1}, 64, nullptr), nullptr)
<< "Magma must not donate a server address as a client persistent map";
}
EXPECT_FALSE(ClientSessionInstance().RunAheadArmed());
EXPECT_TRUE(ClientSessionInstance().RunAheadArmed());
ClientSessionInstance().Stop();
MG_Remote::Server::ServerLoopInstance().Stop();
std::fflush(nullptr);