diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index af59f21a..9fa27748 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -1935,6 +1935,55 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" ) + # ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) -------------------------------- + # + # THE LANE CASE THAT PROVES THE RECORDS CROSSED, and it is a RESULT and not a probe (R-16: + # a probe may not arm against a stub). Each of these four scenarios asserts a value that + # only exists if the verb ran on the apply thread - the texel a compute shader stored + # through an image unit, the word glCopyImageSubData moved, the counter an atomic add left + # behind, the block a program pipeline rebound - so a sink that DECLINED, or a client that + # fell through to the driver, is red by the number it reads back and not by a tally. + # + # Before i1 every one of these aborted at the client's Fatal{UnmigratedVerb} on the first + # glBindImageTexture / glDispatchCompute / glCopyImageSubData / glShaderStorageBlockBinding; + # all four are green under inproc now (the census: 13 + 6 + 11 + 3 DirectGLES entries). + # RED ONCE BY DOING X: return false from ServerVerbSink::OnLaunchGrid before the + # gl.DispatchCompute call and UnboundImageDescriptor / AtomicCounter / ProgramPipeline all + # read back their initial values. + # + # THE FILTERS ARE NARROWED TO THE CASES THAT ACTUALLY EMIT, and that is ScenarioFixture's + # rule rather than a convenience: an armed `DirectGLES.Split.` case must move the client + # encoder's record ordinal (ScenarioFixture.h:86), because a lane whose workload produced no + # record is the "resolved the transport and then fell through to the driver" shape that + # every pixel assertion is blind to. ProgramPipelineScenario's other nine cases are pure + # name/state cases that draw nothing, so the whole-scenario filter would arm them and they + # would be red for having nothing to say. The two named here are the ones that were + # Fatal{UnmigratedVerb, "ShaderStorageBlockBinding"} on the c0b head. + # + # One block per scenario, following the three above: a `:`-separated multi-pattern + # TEST_FILTER is unproven through gtest_discover_tests' flat PROPERTIES forwarding. + set(MGL_SPLIT_I1_SCENARIOS + UnboundImageDescriptorScenario # bind_shader_image (72) + launch_grid (60), 13 cases + CopyImageLayeredScenario # resource_copy_region (53), 6 cases + AtomicCounterScenario # launch_grid (60) + memory_barrier (61), 3 cases + ProgramPipelineScenario) # set_storage_block_binding (75), the 2 storage-block cases + set(MGL_SPLIT_I1_FILTER_UnboundImageDescriptorScenario "UnboundImageDescriptorScenario.*") + set(MGL_SPLIT_I1_FILTER_CopyImageLayeredScenario "CopyImageLayeredScenario.*") + set(MGL_SPLIT_I1_FILTER_AtomicCounterScenario "AtomicCounterScenario.*") + set(MGL_SPLIT_I1_FILTER_ProgramPipelineScenario "ProgramPipelineScenario.*StorageBlock*") + foreach(mglItestI1Scenario IN LISTS MGL_SPLIT_I1_SCENARIOS) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Split." + TEST_LIST "MGL_SPLIT_I1_${mglItestI1Scenario}_TESTS" + TEST_FILTER "${MGL_SPLIT_I1_FILTER_${mglItestI1Scenario}}" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" + ) + endforeach() + # The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split # the adopt tier is pinned at T2, the resource owner declines every acquisition and the client # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in index 532ba453..5ae4a1ce 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -11,4 +11,13 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@) "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") endforeach() endforeach() +# P5b package i1's four scenarios, the same shape as the small-ring loop above: every +# DirectGLES.Split. entry owns exactly one absolute log path and shares it with nobody, which +# is what SplitLogPaths.PrivateAndDistinct checks over the discovered set. +foreach(scenario @MGL_SPLIT_I1_SCENARIOS@) + foreach(entry IN LISTS MGL_SPLIT_I1_${scenario}_TESTS) + set_tests_properties("${entry}" PROPERTIES ENVIRONMENT + "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") + endforeach() +endforeach() # PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it. diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index 068e9080..039a3b10 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -254,13 +254,23 @@ TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) { EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u), MGPipeFieldOwnership::kFatal); - // The three off the reduced path, each for its own checkable reason. + // The two off the reduced path, each for its own checkable reason. THREE UNTIL P5b: the + // third was GetProgramForDispatch, FATAL because "there is no compute on the reduced path", + // and package i1 is what put compute on the path (CONTRACT-P5B.md §6.9). It is asserted + // below in its new class rather than deleted from this case, because a field that quietly + // left the FATAL list is exactly what this case exists to catch. EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName), MGPipeFieldOwnership::kFatal); EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter), MGPipeFieldOwnership::kFatal); + // P5b i1: launch_grid (60) crosses, the backend's PrepareForCompute pulls the compute + // program inside it (DirectGLES.cpp:5779), and the field takes GetProgramForDraw's class + // and its retiring phases - so it is a measured DEBT now, not a defect. EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch), - MGPipeFieldOwnership::kFatal); + MGPipeFieldOwnership::kBarrierPulled); + EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch), + MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDraw)) + << "GetProgramForDispatch is GetProgramForDraw's twin and must share its class"; } TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) { @@ -516,17 +526,43 @@ TEST_F(FieldOwnershipTest, StrictErrorsAlsoPromotesTheStickyForwards) { // A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never // reads it, so it is a real defect rather than a debt. +// P5b i1: the exemplar MOVED. This case used GetProgramForDispatch, which is BARRIER-PULLED +// from i1 on (a debt the server serves, not an abort), so it would now assert that a served +// read aborts - green for the wrong reason at best. GetTransformFeedbackPausedPrimitiveCounter +// is the same statement with a field that is still FATAL: reachable only from class kQuery, +// which the reduced path never enters. +// RED ONCE BY DOING X: put GetProgramForDispatch back in the FATAL block of FieldOwnership.def +// and TheFieldOwnershipTableIsTheContractsTableRow's new kBarrierPulled expectation goes red by +// name; swap the field below for GetProgramForDispatch and THIS case goes red instead, because +// a barrier-pulled read under a stamp does not abort. TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) { const ChildResult r = RunInChild([] { MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays); - (void)gPipeInputs.GetProgramForDispatch(); + (void)gPipeInputs.GetTransformFeedbackPausedPrimitiveCounter(); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DrawArrays\"}"), + EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, " + "\"GetTransformFeedbackPausedPrimitiveCounter@DrawArrays\"}"), std::string::npos) << r.Log; } +// And the field that LEFT the FATAL class is served rather than fatal, under the verb that made +// it reachable. This is i1's half of the §6.9 grant made checkable: a dispatch stamp plus a read +// of the compute program must NOT abort, which is precisely the statement "compute is on the +// path now". RED ONCE BY DOING X: revert the FieldOwnership.def row to FATAL and this child +// aborts with Fatal{UnmigratedPipeInput, "GetProgramForDispatch@DispatchCompute"}. +TEST_F(FieldOwnershipTest, TheComputeProgramIsServedUnderADispatchStampFromP5bOn) { + const ChildResult r = RunInChild([] { + MGPipeServerStampVerbBoundary(MGPipeVerb::DispatchCompute); + (void)gPipeInputs.GetProgramForDispatch(); + MGPipeServerClearVerbBoundary(); + }); + EXPECT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_EQ(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch"), std::string::npos) + << r.Log; +} + // The argument-keyed row: the pack half is answerable and the unpack half is not, and the // difference is the accessor's own argument rather than a second field id. Splitting the field // into two ids would have made the unpack half BARRIER-PULLED - silently served - which is diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index acef2348..238807fe 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -171,12 +171,21 @@ namespace { // ===================================================================================== TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { - // CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal. Read from the functions the - // table itself reports with - which is also what t1's arming condition reads - rather than - // recomputed here, so a table that lost an emitter cannot look like one that never had it. + // CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal at the P5b contract commit. + // Read from the functions the table itself reports with - which is also what t1's arming + // condition reads - rather than recomputed here, so a table that lost an emitter cannot look + // like one that never had it. + // + // P5b package i1 (CONTRACT-P5B.md §2 i1) flipped SEVEN slots C -> B: BindImageTexture, + // DispatchCompute, DispatchComputeIndirect, MemoryBarrier, MemoryBarrierByRegion, + // CopyImageSubData, ShaderStorageBlockBinding. So the two numbers that move are 5 -> 12 and + // 64 -> 57, and the SUM below is the invariant that does not move whichever package lands + // next. RED ONCE BY DOING X: comment out `table.GL.MemoryBarrier = &EmitMemoryBarrier;` in + // BuildRemoteEmitTable and this case stays green while NoSlotIsNull goes red - which is why + // the per-package count is asserted here and the null walk is a separate case. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 5u); - EXPECT_EQ(UnmigratedSlotCount(), 64u); + EXPECT_EQ(ImplementedVerbCount(), 12u); + EXPECT_EQ(UnmigratedSlotCount(), 57u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); } @@ -257,6 +266,49 @@ TEST(RemoteEmitTable, SetSwapIntervalIsClassCAndSaysSo) { EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log; } +#endif // MGTEST_HAVE_FORK + +// ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) ------------------------------------- + +TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) { + // The census's five measured slots plus the two companions that share their rows. Named + // rather than counted, so a table that flipped a DIFFERENT seven is red here and not only + // in the arithmetic. The comparison is against a slot that is still class C: a flipped slot + // and an unflipped one cannot be the same pointer, which is what a forgotten class-B + // assignment would look like (class C is assigned FIRST in BuildRemoteEmitTable precisely so + // that the mistake is loud rather than null). + const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable(); + const void* fatal = reinterpret_cast(table.GL.GetTexImage); // wave-3 tail, class C + ASSERT_NE(fatal, nullptr); + const void* const i1[] = { + reinterpret_cast(table.GL.BindImageTexture), + reinterpret_cast(table.GL.DispatchCompute), + reinterpret_cast(table.GL.DispatchComputeIndirect), + reinterpret_cast(table.GL.MemoryBarrier), + reinterpret_cast(table.GL.MemoryBarrierByRegion), + reinterpret_cast(table.GL.CopyImageSubData), + reinterpret_cast(table.GL.ShaderStorageBlockBinding), + }; + static const char* const kNames[] = {"BindImageTexture", "DispatchCompute", + "DispatchComputeIndirect", "MemoryBarrier", + "MemoryBarrierByRegion", "CopyImageSubData", + "ShaderStorageBlockBinding"}; + for (SizeT i = 0; i < sizeof(i1) / sizeof(i1[0]); ++i) { + EXPECT_NE(i1[i], nullptr) << kNames[i] << " is null"; + EXPECT_NE(i1[i], fatal) << kNames[i] + << " still points at an UnmigratedVerbFatal thunk; i1 flipped it " + "to class B"; + } + // The two barrier slots and the two dispatch slots share a WIRE ROW but not an emitter: the + // discriminant (ByRegion / IsIndirect) is set by the emitter, so one thunk for both would + // carry the wrong one. + EXPECT_NE(i1[3], i1[4]) << "MemoryBarrier and MemoryBarrierByRegion share memory_barrier (61) " + "but must set opposite ByRegion values"; + EXPECT_NE(i1[1], i1[2]) << "DispatchCompute and DispatchComputeIndirect share launch_grid (60) " + "but must set opposite IsIndirect values"; +} + +#if MGTEST_HAVE_FORK TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) { // The other half of "no slot may fall through to the driver". With no ClientSession the // emitter has nowhere to put the record, and the one thing it may not do is return quietly: @@ -265,6 +317,53 @@ TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) { ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log; } + +TEST(RemoteEmitTable, EachI1SlotReachesRequireSessionUnderItsOwnName) { + // The behavioural half: an i1 slot is class B, so with no ClientSession it reaches + // RequireSession and aborts Fatal{NoClientSession, ""} - NOT Fatal{UnmigratedVerb} + // (which would mean the flip never happened) and NOT quietly (which is the split lane + // running monolith and going green, R-4). The name in the message is the slot's own, which + // is the half a single case could not state. + // + // RED ONCE BY DOING X: put `X(MemoryBarrier, void, (GLbitfield))` back in + // MGR_UNMIGRATED_I1_SLOTS and drop `table.GL.MemoryBarrier = &EmitMemoryBarrier;` - the + // MemoryBarrier arm below then finds Fatal{UnmigratedVerb, "MemoryBarrier"} instead. + const ChildResult barrier = RunInChild([] { RemoteEmitTable().GL.MemoryBarrier(0x2000); }); + ASSERT_TRUE(DiedOfAbort(barrier)) << DescribeStatus(barrier) << "\n" << barrier.Log; + EXPECT_NE(barrier.Log.find("Fatal{NoClientSession, \"MemoryBarrier\"}"), std::string::npos) + << barrier.Log; + EXPECT_EQ(barrier.Log.find("Fatal{UnmigratedVerb"), std::string::npos) + << "MemoryBarrier is class B from P5b i1 on:\n" + << barrier.Log; + + const ChildResult dispatch = RunInChild([] { RemoteEmitTable().GL.DispatchCompute(1, 1, 1); }); + ASSERT_TRUE(DiedOfAbort(dispatch)) << DescribeStatus(dispatch) << "\n" << dispatch.Log; + EXPECT_NE(dispatch.Log.find("Fatal{NoClientSession, \"DispatchCompute\"}"), std::string::npos) + << dispatch.Log; + + const ChildResult bind = RunInChild( + [] { RemoteEmitTable().GL.BindImageTexture(0, 1, 0, GL_FALSE, 0, 0x88BA, 0x8058); }); + ASSERT_TRUE(DiedOfAbort(bind)) << DescribeStatus(bind) << "\n" << bind.Log; + EXPECT_NE(bind.Log.find("Fatal{NoClientSession, \"BindImageTexture\"}"), std::string::npos) + << bind.Log; + + const ChildResult ssbo = RunInChild( + [] { RemoteEmitTable().GL.ShaderStorageBlockBinding(1, "Blk", 2); }); + ASSERT_TRUE(DiedOfAbort(ssbo)) << DescribeStatus(ssbo) << "\n" << ssbo.Log; + EXPECT_NE(ssbo.Log.find("Fatal{NoClientSession, \"ShaderStorageBlockBinding\"}"), + std::string::npos) + << ssbo.Log; + + const ChildResult copy = RunInChild([] { + const MG_Backend::CopyImageEndpoint src{}; + const MG_Backend::CopyImageEndpoint dst{}; + RemoteEmitTable().GL.CopyImageSubData(src, 0x0DE1, 0, 0, 0, 0, dst, 0x0DE1, 0, 0, 0, 0, 1, + 1, 1); + }); + ASSERT_TRUE(DiedOfAbort(copy)) << DescribeStatus(copy) << "\n" << copy.Log; + EXPECT_NE(copy.Log.find("Fatal{NoClientSession, \"CopyImageSubData\"}"), std::string::npos) + << copy.Log; +} #endif // MGTEST_HAVE_FORK // =====================================================================================