diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index fd372811..068e9080 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -292,7 +292,18 @@ TEST_F(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) { EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ResumeStreamOutput), MGPipeVerb::ResumeTransformFeedback); EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GenerateMipmap), MGPipeVerb::GenerateMipmap); EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetTextureImage), MGPipeVerb::GetTextureImage); - EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{12}); + // P5b (MG_Remote/CONTRACT-P5B.md): the renamed boundary the file left to "the phase that + // emits it" - resource_copy_region is glCopyImageSubData only - and the five appended verbs, + // each stamped as the GLFunctionsTable verb it reproduces. copy_framebuffer_to_texture also + // carries glCopyTexSubImage2D and stamps CopyTexImage2D for both: one kBlitOrCopy mask. + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ResourceCopyRegion), MGPipeVerb::CopyImageSubData); + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BindShaderImage), MGPipeVerb::BindImageTexture); + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::PatchParameter), MGPipeVerb::PatchParameteri); + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BindStreamOutput), MGPipeVerb::BindTransformFeedback); + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetStorageBlockBinding), + MGPipeVerb::ShaderStorageBlockBinding); + EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::CopyFramebufferToTexture), MGPipeVerb::CopyTexImage2D); + EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{18}); EXPECT_EQ(kMGPipeVerbBoundaryExemptCount, SizeT{3}); // Present is class B (it is emitted in P5) and is STILL not a verb boundary: diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index aa353029..6188d943 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -130,7 +130,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { EXPECT_EQ(ClassCount(), 13u); EXPECT_EQ(ClassCount(), 17u); EXPECT_EQ(ClassCount(), 9u); - EXPECT_EQ(ClassCount(), 13u); + // 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image, + // patch_parameter, bind_stream_output, set_storage_block_binding, + // copy_framebuffer_to_texture. + EXPECT_EQ(ClassCount(), 18u); } // A row nobody has migrated is null - which is exactly what "this subsystem has not been @@ -545,7 +548,41 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { EXPECT_EQ(static_cast(MGPWireOp::QueryTimestamp), 69); EXPECT_EQ(static_cast(MGPWireOp::QueryCounter), 70); EXPECT_EQ(static_cast(MGPWireOp::FenceWaitServer), 71); - EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 72); + // P5b (MG_Remote/CONTRACT-P5B.md) appended five verbs AFTER P0's three late arrivals, by + // the same rule: opcodes 72..76, and nothing before them moved. The five are pinned by + // VALUE, because the pin is the protocol - a row inserted ahead of one would silently + // re-point every record a P5b package emits at the wrong arm. + EXPECT_EQ(static_cast(MGPWireOp::BindShaderImage), 72); + EXPECT_EQ(static_cast(MGPWireOp::PatchParameter), 73); + EXPECT_EQ(static_cast(MGPWireOp::BindStreamOutput), 74); + EXPECT_EQ(static_cast(MGPWireOp::SetStorageBlockBinding), 75); + EXPECT_EQ(static_cast(MGPWireOp::CopyFramebufferToTexture), 76); + EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 77); + // And the P5b rows carry what their contract says: one blob (the block name) and nothing + // else, and the extended draw row keeps its two flags. + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast(kHasBlob)); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindShaderImage), static_cast(kNone)); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::PatchParameter), static_cast(kNone)); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindStreamOutput), static_cast(kNone)); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::CopyFramebufferToTexture), static_cast(kNone)); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::DrawVbo), static_cast(kHostSpan | kVarTail)); + // The P5b payload sizes, pinned like every other MGP_ASSERT_POD at runtime so the numbers + // show up in ctest output; MGPCopyRegion grew 64 -> 72 for its two GL names (i1). + EXPECT_EQ(sizeof(MGPImageBind), 40u); + EXPECT_EQ(sizeof(MGPPatchParameter), 8u); + EXPECT_EQ(sizeof(MGPStreamOutputBind), 16u); + EXPECT_EQ(sizeof(MGPStorageBlockBinding), 40u); + EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u); + EXPECT_EQ(sizeof(MGPCopyRegion), 72u); + EXPECT_EQ(sizeof(MGPDrawIndirect), 40u); + // The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value. + EXPECT_EQ(static_cast(kDrawIsIndirect), 1u << 5); + EXPECT_EQ(static_cast(kDrawHasUserIndices) & static_cast(kDrawIsIndirect), 0u); + // The clear discriminants have ONE spelling now, and Whole is 0 so a zeroed record is a + // whole-framebuffer clear. + EXPECT_EQ(kMGPipeClearKindWhole, 0u); + EXPECT_EQ(kMGPipeClearKindDepthStencil + 1, kMGPipeClearKindCount); + EXPECT_EQ(kMGPipeClearValueClassUint + 1, kMGPipeClearValueClassCount); } // A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted" @@ -732,7 +769,10 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) { // a cache that read the padding would mint a fresh CSO per call and the verify lane would // abort at random. TEST(PipeCatalogue, SixValueStructsHaveFieldLists) { - EXPECT_EQ(kMGPipeVerifiedPayloadCount, 72u); + // 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind, + // MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer), + // each with its own field list, so the comparator sees every one of them: 77. + EXPECT_EQ(kMGPipeVerifiedPayloadCount, 77u); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index 4e1b8508..7b909290 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -192,7 +192,7 @@ namespace { return true; } Bool OnDrawVbo(const MGPDrawInfo& info, const MGPDrawRange* ranges, - const MGHostSpan* userIndices) override { + const MGHostSpan* userIndices, const MGPDrawIndirect* indirect) override { Draws.push_back(info); DrawRanges.clear(); for (Uint32 i = 0; i < info.NumDraws; ++i) { @@ -202,6 +202,68 @@ namespace { if (userIndices != nullptr) { LastSpan = *userIndices; } + SawIndirect = indirect != nullptr; + if (indirect != nullptr) { + LastIndirect = *indirect; + } + return true; + } + // ---- P5b's rows (CONTRACT-P5B.md): one recorder per sink method, so a round trip + // asserts the arm reached the RIGHT consumer with the record intact. + Bool OnLaunchGrid(const MGPGridInfo& grid) override { + Grids.push_back(grid); + return true; + } + Bool OnMemoryBarrier(const MGPMemoryBarrier& barrier) override { + Barriers.push_back(barrier); + return true; + } + Bool OnResourceCopyRegion(const MGPCopyRegion& copy) override { + Copies.push_back(copy); + return true; + } + Bool OnBindShaderImage(const MGPImageBind& bind) override { + ImageBinds.push_back(bind); + return true; + } + Bool OnSetStorageBlockBinding(const MGPStorageBlockBinding& binding, + const char* name) override { + StorageBindings.push_back(binding); + StorageBlockNames.push_back(name != nullptr ? name : ""); + return true; + } + Bool OnBeginStreamOutput(const MGPStreamOutputBegin& begin) override { + Begins.push_back(begin); + return true; + } + Bool OnEndStreamOutput(const MGPXfbAccounting& accounting) override { + Ends.push_back(accounting); + return true; + } + Bool OnPauseStreamOutput(const MGPStreamOutputControl& control) override { + (void)control; + ++Pauses; + return true; + } + Bool OnResumeStreamOutput(const MGPStreamOutputControl& control) override { + (void)control; + ++Resumes; + return true; + } + Bool OnBindStreamOutput(const MGPStreamOutputBind& bind) override { + StreamOutputBinds.push_back(bind); + return true; + } + Bool OnPatchParameter(const MGPPatchParameter& patch) override { + Patches.push_back(patch); + return true; + } + Bool OnGenerateMipmap(const MGPMipPlan& plan) override { + MipPlans.push_back(plan); + return true; + } + Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& copy) override { + FramebufferCopies.push_back(copy); return true; } std::vector Clears; @@ -213,6 +275,22 @@ namespace { std::vector DrawRanges; bool SawUserIndices = false; MGHostSpan LastSpan{}; + bool SawIndirect = false; + MGPDrawIndirect LastIndirect{}; + std::vector Grids; + std::vector Barriers; + std::vector Copies; + std::vector ImageBinds; + std::vector StorageBindings; + std::vector StorageBlockNames; + std::vector Begins; + std::vector Ends; + Uint32 Pauses = 0; + Uint32 Resumes = 0; + std::vector StreamOutputBinds; + std::vector Patches; + std::vector MipPlans; + std::vector FramebufferCopies; }; Replies& Answers() { return m_replies; } @@ -921,6 +999,267 @@ TEST_F(PipeWireCodecTest, DrawVboConditionalSpanTailIsEightAlignedAndSurvives) { EXPECT_TRUE(wire.Sink().SawUserIndices); EXPECT_EQ(wire.Sink().LastSpan.Ptr, nullptr); EXPECT_EQ(wire.Sink().LastSpan.Size, sizeof(indices)); + EXPECT_FALSE(wire.Sink().SawIndirect); +} + +// ===================================================================================== +// P5b (MG_Remote/CONTRACT-P5B.md): one round trip per row the four migration packages consume +// ===================================================================================== +// +// The shape is the one above: encode, pump one record through the ring and the decoder, and +// assert that the arm reached the RIGHT sink method with the record intact. Nothing here +// asserts rendering - the sink is a recorder - which is exactly the layer the contract package +// owns: the bytes and the dispatch, not the backend call. + +TEST_F(PipeWireCodecTest, DrawVboIndirectTailCrossesInPlaceOfTheSpan) { + // d1: kDrawIsIndirect puts one MGPDrawIndirect where the user-index span would sit, with + // NumDraws 0 (the server never reads the indirect buffer to learn a count). + Wire2 wire; + MGPDrawInfo info{}; + info.Mode = 4; + info.IndexSize = 4; + info.Flags = kDrawIsIndirect; + info.InstanceCount = 1; + info.NumDraws = 0; + MGPDrawIndirect indirect{}; + indirect.Buffer = MakeHandle(81); + indirect.ParameterBuffer = MakeHandle(82); + indirect.Offset = 64; + indirect.ParameterOffset = 16; + indirect.Stride = 20; + indirect.DrawCount = 7; + + WireRecordLayout layout{}; + ASSERT_TRUE(MGPipeWireRecordLayout(MGPWireOp::DrawVbo, &info, layout)); + EXPECT_EQ(layout.TailCount, 2u); + EXPECT_EQ(layout.TailBytes[0], 0u); + EXPECT_EQ(layout.TailBytes[1], sizeof(MGPDrawIndirect)); + EXPECT_FALSE(layout.SecondTailIsHostSpans); + EXPECT_EQ(layout.TailOffset[1] % 8, 0u); + + const WireTail tails[2] = {{nullptr, 0}, {&indirect, sizeof(indirect)}}; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::DrawVbo, &info, sizeof(info), tails, 2), + kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().Draws.size(), 1u); + EXPECT_FALSE(wire.Sink().SawUserIndices); + ASSERT_TRUE(wire.Sink().SawIndirect); + EXPECT_EQ(wire.Sink().LastIndirect.Buffer.Slot, 81u); + EXPECT_EQ(wire.Sink().LastIndirect.ParameterBuffer.Slot, 82u); + EXPECT_EQ(wire.Sink().LastIndirect.Offset, 64u); + EXPECT_EQ(wire.Sink().LastIndirect.ParameterOffset, 16u); + EXPECT_EQ(wire.Sink().LastIndirect.Stride, 20u); + EXPECT_EQ(wire.Sink().LastIndirect.DrawCount, 7u); +} + +TEST_F(PipeWireCodecTest, LaunchGridReachesTheSink) { + Wire2 wire; + MGPGridInfo grid{}; + grid.GridX = 8; + grid.GridY = 4; + grid.GridZ = 2; + grid.IsIndirect = 0; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::LaunchGrid, &grid, sizeof(grid)), kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().Grids.size(), 1u); + EXPECT_EQ(wire.Sink().Grids[0].GridX, 8u); + EXPECT_EQ(wire.Sink().Grids[0].GridZ, 2u); +} + +TEST_F(PipeWireCodecTest, MemoryBarrierReachesTheSink) { + Wire2 wire; + MGPMemoryBarrier barrier{}; + barrier.Bits = 0x2000u; // GL_SHADER_STORAGE_BARRIER_BIT + barrier.ByRegion = 0; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)), + kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().Barriers.size(), 1u); + EXPECT_EQ(wire.Sink().Barriers[0].Bits, 0x2000u); +} + +TEST_F(PipeWireCodecTest, ResourceCopyRegionReachesTheSinkWithItsTwoNames) { + Wire2 wire; + MGPCopyRegion copy{}; + copy.Src = MakeHandle(91); + copy.Dst = MakeHandle(92); + copy.SrcBox = MGPBox{1, 2, 0, 16, 8, 1}; + copy.DstX = 3; + copy.DstY = 4; + copy.DstZ = 0; + copy.SrcTarget = 0x0DE1; // GL_TEXTURE_2D + copy.DstTarget = 0x0DE1; + copy.SrcLevel = 0; + copy.DstLevel = 1; + copy.SrcGlName = 1001; + copy.DstGlName = 1002; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceCopyRegion, ©, sizeof(copy)), + kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().Copies.size(), 1u); + EXPECT_EQ(wire.Sink().Copies[0].SrcGlName, 1001u); + EXPECT_EQ(wire.Sink().Copies[0].DstGlName, 1002u); + EXPECT_EQ(wire.Sink().Copies[0].SrcBox.W, 16u); + EXPECT_EQ(wire.Sink().Copies[0].DstLevel, 1u); +} + +TEST_F(PipeWireCodecTest, GenerateMipmapReachesTheSink) { + Wire2 wire; + MGPMipPlan plan{}; + plan.Res = MakeHandle(93); + plan.Target = 0x8513; // GL_TEXTURE_CUBE_MAP, verbatim + plan.BaseLevel = 0; + plan.LevelCount = 9; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::GenerateMipmap, &plan, sizeof(plan)), kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().MipPlans.size(), 1u); + EXPECT_EQ(wire.Sink().MipPlans[0].Target, 0x8513u); + EXPECT_EQ(wire.Sink().MipPlans[0].LevelCount, 9u); +} + +TEST_F(PipeWireCodecTest, TheFourStreamOutputSpanRowsReachTheSink) { + Wire2 wire; + MGPStreamOutputBegin begin{}; + begin.PrimitiveMode = 4; + MGPStreamOutputControl control{}; + MGPXfbAccounting end{}; + end.CapturedVertices = 300; + end.PrimitivesWritten = 100; + end.PrimitiveMode = 4; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BeginStreamOutput, &begin, sizeof(begin)), kInvalidSeq); + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::PauseStreamOutput, &control, sizeof(control)), kInvalidSeq); + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResumeStreamOutput, &control, sizeof(control)), kInvalidSeq); + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::EndStreamOutput, &end, sizeof(end)), kInvalidSeq); + bool applied = false; + for (int i = 0; i < 4; ++i) { + ASSERT_TRUE(wire.PumpOne(&applied)) << i; + EXPECT_TRUE(applied) << i; + } + ASSERT_EQ(wire.Sink().Begins.size(), 1u); + EXPECT_EQ(wire.Sink().Begins[0].PrimitiveMode, 4u); + EXPECT_EQ(wire.Sink().Pauses, 1u); + EXPECT_EQ(wire.Sink().Resumes, 1u); + ASSERT_EQ(wire.Sink().Ends.size(), 1u); + EXPECT_EQ(wire.Sink().Ends[0].CapturedVertices, 300u); + EXPECT_EQ(wire.Decoder().AppliedSeq(), 4u); +} + +TEST_F(PipeWireCodecTest, BindShaderImageReachesTheSinkVerbatim) { + Wire2 wire; + MGPImageBind bind{}; + bind.Res = MakeHandle(94); + bind.Unit = 3; + bind.GlName = 77; + bind.Level = 1; + bind.Layer = 5; + bind.Layered = 1; + bind.Access = 0x88BA; // GL_READ_WRITE, the GL token and not the view encoding + bind.Format = 0x8058; // GL_RGBA8 + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindShaderImage, &bind, sizeof(bind)), kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().ImageBinds.size(), 1u); + EXPECT_EQ(wire.Sink().ImageBinds[0].Unit, 3u); + EXPECT_EQ(wire.Sink().ImageBinds[0].Access, 0x88BAu); + EXPECT_EQ(wire.Sink().ImageBinds[0].Layered, 1u); + EXPECT_EQ(wire.Sink().ImageBinds[0].Layer, 5); +} + +TEST_F(PipeWireCodecTest, PatchParameterReachesTheSink) { + Wire2 wire; + MGPPatchParameter patch{}; + patch.Pname = 0x8E72; // GL_PATCH_VERTICES + patch.Value = 16; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::PatchParameter, &patch, sizeof(patch)), kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().Patches.size(), 1u); + EXPECT_EQ(wire.Sink().Patches[0].Pname, 0x8E72u); + EXPECT_EQ(wire.Sink().Patches[0].Value, 16); +} + +TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) { + Wire2 wire; + MGPStreamOutputBind bind{}; + bind.GlName = 12; + bind.LifetimeId = 0x1234567890ull; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindStreamOutput, &bind, sizeof(bind)), kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().StreamOutputBinds.size(), 1u); + EXPECT_EQ(wire.Sink().StreamOutputBinds[0].GlName, 12u); + EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull); +} + +TEST_F(PipeWireCodecTest, SetStorageBlockBindingCarriesItsNameAsAStagedBlob) { + // i1: the ONE string on the wire. Size is strlen + 1 - the NUL travels - and the decoder + // hands the sink a pointer that dies with the call. + Wire2 wire; + const char name[] = "ParticleBuffer"; + MGPStorageBlockBinding binding{}; + binding.ShaderCso = MakeHandle(95); + binding.GlName = 501; + binding.Binding = 2; + binding.Name = wire.Encoder().StageBytes(name, sizeof(name)); + EXPECT_EQ(binding.Name.Size, sizeof(name)); + EXPECT_EQ(binding.Name.Seg, static_cast(kSegStage)); + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetStorageBlockBinding, &binding, sizeof(binding)), + kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().StorageBindings.size(), 1u); + EXPECT_EQ(wire.Sink().StorageBindings[0].Binding, 2u); + EXPECT_EQ(wire.Sink().StorageBindings[0].GlName, 501u); + ASSERT_EQ(wire.Sink().StorageBlockNames.size(), 1u); + EXPECT_EQ(wire.Sink().StorageBlockNames[0], "ParticleBuffer"); +} + +TEST_F(PipeWireCodecTest, CopyFramebufferToTextureReachesTheSinkForBothForms) { + Wire2 wire; + MGPCopyFromFramebuffer image{}; + image.Dst = MakeHandle(96); + image.Target = 0x0DE1; // GL_TEXTURE_2D + image.Level = 0; + image.InternalFormat = 0x8058; + image.X = 10; + image.Y = 20; + image.Width = 64; + image.Height = 32; + image.SubImage = 0; + MGPCopyFromFramebuffer sub = image; + sub.SubImage = 1; + sub.InternalFormat = 0; + sub.XOffset = 4; + sub.YOffset = 8; + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CopyFramebufferToTexture, &image, sizeof(image)), + kInvalidSeq); + ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CopyFramebufferToTexture, &sub, sizeof(sub)), + kInvalidSeq); + bool applied = false; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + ASSERT_EQ(wire.Sink().FramebufferCopies.size(), 2u); + EXPECT_EQ(wire.Sink().FramebufferCopies[0].SubImage, 0u); + EXPECT_EQ(wire.Sink().FramebufferCopies[0].InternalFormat, 0x8058u); + EXPECT_EQ(wire.Sink().FramebufferCopies[1].SubImage, 1u); + EXPECT_EQ(wire.Sink().FramebufferCopies[1].XOffset, 4); + EXPECT_EQ(wire.Sink().FramebufferCopies[1].Width, 64); } // ===================================================================================== @@ -1874,6 +2213,41 @@ TEST_F(PipeWireCodecTest, AHostSpanRunPastItsSegmentIsFatalOnDrawVbo) { // ---- M5 / m8 / q2 ------------------------------------------------------------------------ +TEST_F(PipeWireCodecTest, ADrawThatSetsBothTheSpanAndTheIndirectFlagIsFatal) { + // P5b d1: the two second-tail flags are exclusive by contract - an indirect draw's indices + // come from the bound element buffer - and the layout is where the exclusion is enforced, + // on BOTH sides, so it is forged straight into SEG_CMD here rather than encoded. + Wire2 wire; + MGPDrawInfo info{}; + info.Mode = 4; + info.IndexSize = 2; + info.Flags = kDrawHasUserIndices | kDrawIsIndirect; + info.InstanceCount = 1; + info.NumDraws = 0; + MGPDrawIndirect indirect{}; + const ChildResult r = RunInChild([&] { + ForgeAndDecode(wire, MGPWireOp::DrawVbo, &info, sizeof(info), &indirect, sizeof(indirect)); + }); + EXPECT_TRUE(DiedOfAbort(r)) << DescribeStatus(r); + EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"DrawVbo.Flags\"}"), std::string::npos) << r.Log; +} + +TEST_F(PipeWireCodecTest, AnIndirectDrawThatDeclaresRangesIsFatal) { + Wire2 wire; + MGPDrawInfo info{}; + info.Mode = 4; + info.IndexSize = 0; + info.Flags = kDrawIsIndirect; + info.InstanceCount = 1; + info.NumDraws = 1; // the server never reads a count and the client never sends ranges + MGPDrawRange range{0, 3, 0}; + const ChildResult r = RunInChild([&] { + ForgeAndDecode(wire, MGPWireOp::DrawVbo, &info, sizeof(info), &range, sizeof(range)); + }); + EXPECT_TRUE(DiedOfAbort(r)) << DescribeStatus(r); + EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"DrawVbo.NumDraws\"}"), std::string::npos) << r.Log; +} + TEST_F(PipeWireCodecTest, BufferSubDataResidentMayNotDeclareRegions) { // Its catalogue row has no kVarTail and its applier takes no regions, so a non-zero // RegionCount is a fault rather than a tail. The layout used to share ResourceSubData's