From c1960fdfaada0d964f1d8fafa953ef2d8cd2c1a1 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 04:02:44 -0400 Subject: [PATCH] [MG_IntegrationTest] (Disaggregated): prove real Magma queued state and present credit --- .github/workflows/test.yml | 25 ++ MobileGL/MG_IntegrationTest/CMakeLists.txt | 33 ++ .../Harness/SplitRuntimePeek.cpp | 72 ++++ .../Harness/SplitRuntimePeek.h | 18 + .../Scenarios/MagmaRunAheadScenario.cpp | 328 ++++++++++++++++++ .../MG_Test/Pipe/MagmaPipeIdentityTest.cpp | 19 +- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 5 +- 7 files changed, 486 insertions(+), 14 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/MagmaRunAheadScenario.cpp diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 028c2576..c1b2a989 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index c0b8627e..b81a9b68 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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: diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp index 81a3b992..e0c29afb 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp @@ -21,11 +21,74 @@ #include #include #include +#include #include #define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1 #endif namespace MGITest { + +#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE) + namespace { + std::atomic 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. diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h index f4e7ad88..0c519ec1 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h +++ b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/MagmaRunAheadScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/MagmaRunAheadScenario.cpp new file mode 100644 index 00000000..c629514d --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/MagmaRunAheadScenario.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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; +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 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> 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{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 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 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(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 oldPixels{255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255}; + const std::array 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 bytes{}; + for (size_t i=0;i 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()(MG_Pipe::kCapRunAheadApply), - 0u); - EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) & - static_cast(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(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 diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index d279cef4..a938a23e 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -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(kCapRunAheadApply), 0u); + EXPECT_EQ(mask & static_cast(kCapRunAheadApply), static_cast(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);