diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 4c2ec2c3..db4083cf 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -11,11 +11,42 @@ // (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test // (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic. // -// X(Name, PayloadStruct, Class, Flags) +// X(Name, PayloadStruct, Class, Flags, WaitClass) // Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery // kScreen lands in struct MGPipeScreen, every other class in struct // MGPipeContext (plan section 4.3). // Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional +// WaitClass : kWaitNone | kWaitApplied | kWaitReply | kWaitPresent +// P5e (MG_Remote/CONTRACT-P5E.md section 2.2). WHAT THE CLIENT DOES AFTER IT HAS +// PUBLISHED A RECORD OF THIS CALL, on a server that publishes kCapRunAheadApply. +// It is A VALUE AND NOT A NEW OP: the opcode order and the flag column are +// untouched, and gen_pipe.py emits the column beside kMGPipeCallFlags as +// MGPipeWaitClassFor(op) in generated/PipeWire.inc, so the emit table and the +// sink compute the barriered predicate from one table instead of two copies. +// +// kWaitReply the answer is not derivable and the client acts on it (R-5), so +// it blocks on the reply slot. EVERY kReplySlot ROW CARRIES IT and +// gen_pipe.py refuses a row where the two disagree - a reply-slot +// row that did not wait would be a waiter that hangs, and a waiting +// row with no slot would block on an answer nobody posts. +// resource_subdata is here because the row OWNS a slot; the buffer +// half's wantReply=false is a per-record ROUTE decision one level +// down (CONTRACT-P5E.md section 2.5), not a second wait class. +// kWaitPresent present, whose pacing is the present CREDIT taken BEFORE the +// encode rather than a wait after the publish (section 2.4). +// kWaitApplied the record's apply still reads a BARRIER_PULLED row or probes the +// client allocator, so the client parks in its own wait and the row +// keeps CONTRACT-P5C's semantics exactly: its pulls count into rsp +// and its probes are legal inside a scope. This column is the +// static half of MGPipeBarriered(op, payload, applierState); the +// other two escalations (an open XFB span, a draw with client +// vertex arrays) are payload-derived and live in that function. +// kWaitNone rule F holds for this row's sink: its apply names no client +// memory at all, so the client publishes and moves on. +// +// THE COLUMN IS INERT UNTIL THE INTEGRATION COMMIT publishes kCapRunAheadApply +// (MG_Backend/Init.cpp's kMGPipeP5eRunAheadReady): with the bit unpublished the +// client runs today's lockstep path and never reads the table. // kNeedsAck on a call means records of this call MAY require an ack; a // per-record predicate decides. resource_respecify carries it for // glBufferStorage - a real synchronous allocation, and the only entry point @@ -45,15 +76,15 @@ // appended server-side fence wait 1 and P5c's applier_reset 1 // kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2 // kCtxCso 13 CSO create/bind/delete -// kCtxState 18 16 of the 18 set_* calls + the temporary set_residual_value_state -// + P5c rv's set_context_values +// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state +// + P5c rv's set_context_values + P5e's set_program_bindings // kCtxObject 10 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus // P5c's object_death 1 // kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five // P5b-appended verbs (bind_shader_image, patch_parameter, // bind_stream_output, set_storage_block_binding, // copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md) -// total 79 +// total 80 // // Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they // do not add up to a set of UNIQUE records and this file has to hold unique records: @@ -101,9 +132,17 @@ // routed through gMGPipeContext like its seventeen siblings, and under monolith it has no // producer at all - the fields keep coming through the residual fill, byte for byte as // before (G1). 79 unique records. +// - P5e (MG_Remote/CONTRACT-P5E.md section 1) appended ONE more (opcode 80): +// set_program_bindings, the three POST-LINK mutable reflection fields - the uniform-block +// bindings, the sampler uniform units and the name-keyed storage-block override map. They +// live inside the LinkArtifacts the reflection archive snapshots, and all three move AFTER +// the link, so no existing row can carry them: create_shader_state describes the link and +// this one describes what happened to it since. It lands with a NULL ROUTE and a sink that +// refuses it by name until package pg wires the emitter, exactly as set_shader_buffers has +// sat catalogued-and-dead since P4a. 80 unique records. // --------------------------------------------------------------------------------------- -#define MGP_CALL_LIST_DOCUMENTED_COUNT 79 +#define MGP_CALL_LIST_DOCUMENTED_COUNT 80 // clang-format off #define MGP_CALL_LIST(X) \ @@ -112,7 +151,7 @@ /* RendererInfo (MGPipeTypes.h). It carried none until P5 R-13.1; the flag is a */ \ /* property of the payload, so its absence was simply wrong. Adding a flag does not */ \ /* move an opcode: the opcode is this line's 1-based position. */ \ - X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob) \ + X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob, kWaitReply) \ /* THE FOUR ACCEPTANCE ROWS (P5 R-5, R-16). These four and map_persistent are the only */ \ /* applier entry points that RETURN something the client acts on destructively - clearing */ \ /* per-level dirty flags, latching parameters, advancing its own bookkeeping - so the */ \ @@ -120,7 +159,7 @@ /* until P5, because in monolith the answer is the return value of a direct call and there */ \ /* was nothing to declare. kMGPipeCallFlags is now what sizes the reply pool and what the */ \ /* decoder posts against, so a row that answers without the flag is a waiter that hangs. */ \ - X(ResourceCreate, MGPResourceDesc, kScreen, kReplySlot) \ + X(ResourceCreate, MGPResourceDesc, kScreen, kReplySlot, kWaitReply) \ /* NO kHasBlob, BY RULING (P5 R-13.3; CONTRACT-P5.md table 1 row 19). MGPResourceDesc */ \ /* owns no MGPBlobRef and gains none: under split `initialBytes` is always nullptr and */ \ /* the initial content of a glBufferData(size, data) / glTexImage*(..., data) arrives */ \ @@ -129,65 +168,65 @@ /* cost is one extra record. NOTE the SECOND uncarried companion on this call, the */ \ /* MGPRespecifiedLevel* scope (PipeApply.h:792) - it is NOT bytes and NOT covered by */ \ /* this ruling; table 1 row 19 gives it the two pad fields of MGPResourceDesc. */ \ - X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck|kReplySlot) \ - X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \ - X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \ - X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \ - X(FenceCreate, MGPHandleOnly, kScreen, kNone) \ - X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot) \ - X(FenceWait, MGPFenceWait, kScreen, kReplySlot) \ - X(FenceDestroy, MGPHandleOnly, kScreen, kNone) \ + X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck|kReplySlot, kWaitReply) \ + X(ResourceDestroy, MGPHandleOnly, kScreen, kNone, kWaitNone) \ + X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional, kWaitReply) \ + X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional, kWaitNone) \ + X(FenceCreate, MGPHandleOnly, kScreen, kNone, kWaitNone) \ + X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot, kWaitReply) \ + X(FenceWait, MGPFenceWait, kScreen, kReplySlot, kWaitReply) \ + X(FenceDestroy, MGPHandleOnly, kScreen, kNone, kWaitNone) \ /* ---- context: query objects (plan 4.3 gives the namespace to the context) ---- */ \ - X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone) \ - X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone) \ - X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone) \ - X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot) \ - X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot) \ - X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone) \ + X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \ + X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \ + X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \ + X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot, kWaitReply) \ + X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot, kWaitReply) \ + X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone, kWaitNone) \ /* ---- context: CSO create/bind/delete (plan 4.4.2, 4.5.2-4.5.5) ---- */ \ - X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \ - X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \ - X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone) \ - X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \ - X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \ - X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \ + X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob, kWaitNone) \ + X(BindRenderState, MGPBindRenderState, kCtxCso, kNone, kWaitNone) \ + X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ + X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob, kWaitNone) \ + X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ + X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ /* kHasBlob because MGPSamplerDesc owns an MGPBlobRef member, `Parameters` - the */ \ /* SamplerParameters POD byte for byte, borderColorForm included (P5 R-13.1). The */ \ /* companion pointer beside the record today is a TYPED frontend pointer */ \ /* (const SamplerParameters*), which is exactly what split may not carry. */ \ - X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob) \ - X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \ - X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \ - X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \ - X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob) \ - X(BindShaderState, MGPHandleOnly, kCtxCso, kNone) \ - X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone) \ + X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob, kWaitNone) \ + X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ + X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone, kWaitNone) \ + X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ + X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob, kWaitNone) \ + X(BindShaderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ + X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \ /* ---- context: set_* (plan 4.4.3) ---- */ \ - X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \ - X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \ - X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail) \ - X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone) \ - X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone) \ - X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \ - X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail) \ - X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail) \ - X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \ - X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail) \ - X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob) \ - X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail) \ - X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone) \ - X(SetPatchState, MGPPatchState, kCtxState, kNone) \ - X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone) \ - X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone) \ + X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob, kWaitNone) \ + X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone, kWaitNone) \ + X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail, kWaitNone) \ + X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone, kWaitNone) \ + X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone, kWaitNone) \ + X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail, kWaitNone) \ + X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail, kWaitNone) \ + X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail, kWaitNone) \ + X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan, kWaitNone) \ + X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail, kWaitNone) \ + X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob, kWaitNone) \ + X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail, kWaitNone) \ + X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone, kWaitNone) \ + X(SetPatchState, MGPPatchState, kCtxState, kNone, kWaitNone) \ + X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone, kWaitNone) \ + X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone, kWaitNone) \ /* Migration-only carrier for Track V, retired field by field across P2..P13. Its */ \ /* retirement is a compile error: MGL_RESIDUAL_BLOCK_SIZE only ever goes DOWN and the */ \ /* final step asserts sizeof(ResidualValueBlock) == 0 (plan 6.3). */ \ - X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) \ + X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob, kWaitNone) \ /* ---- context: per-object state and transfer (plan 4.4.3 set_texture_params, 4.4.4) ---- */ \ - X(SetTextureParams, MGPTextureParams, kCtxObject, kReplySlot) \ - X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail|kReplySlot) \ - X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \ - X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \ + X(SetTextureParams, MGPTextureParams, kCtxObject, kReplySlot, kWaitReply) \ + X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail|kReplySlot, kWaitReply) \ + X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional, kWaitNone) \ + X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone, kWaitNone) \ /* NO kHasBlob AND NO MGPBlobRef, BY RULING (P5 R-13.2; CONTRACT-P5.md table 1 row 21). */ \ /* It takes a content pointer today, but under split it CARRIES NO BYTES AT ALL: it is */ \ /* a {range, AccessFlags} control record, and the bytes of [Offset, Offset+Size) arrive */ \ @@ -195,36 +234,36 @@ /* drives rewrites its range "from the authoritative shadow" (Managers.cpp:1047-1076), */ \ /* and under split the authoritative shadow is server-owned (R-11), so subdata is the */ \ /* only way bytes reach it - a blobref here would be a SECOND way to say the same thing. */ \ - X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \ - X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \ - X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \ - X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone) \ - X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot) \ + X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone, kWaitNone) \ + X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot, kWaitReply) \ + X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone, kWaitNone) \ + X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone, kWaitApplied) \ + X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot, kWaitReply) \ /* ---- context: transfer calls that read whole-context state, and the commands ---- */ \ - X(Blit, MGPBlit, kCtxVerb, kNone) \ - X(Clear, MGPClear, kCtxVerb, kNone) \ - X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot) \ - X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \ - X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone) \ - X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone) \ - X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone) \ - X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone) \ - X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \ - X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \ - X(Flush, MGPFlush, kCtxVerb, kNone) \ - X(Present, MGPPresent, kCtxVerb, kNone) \ - X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) \ + X(Blit, MGPBlit, kCtxVerb, kNone, kWaitNone) \ + X(Clear, MGPClear, kCtxVerb, kNone, kWaitNone) \ + X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot, kWaitReply) \ + X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail, kWaitNone) \ + X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone, kWaitNone) \ + X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone, kWaitNone) \ + X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone, kWaitApplied) \ + X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone, kWaitApplied) \ + X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \ + X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \ + X(Flush, MGPFlush, kCtxVerb, kNone, kWaitNone) \ + X(Present, MGPPresent, kCtxVerb, kNone, kWaitPresent) \ + X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional, kWaitNone) \ /* ---- APPENDED. Opcodes are positional, so a late arrival goes at the END, never into ---- */ \ /* ---- its group: three live GLFunctionsTable entries the catalogue had no carrier for. ---- */ \ /* glGetInteger64v(GL_TIMESTAMP) - GetGpuTimestampNs, a synchronous server answer, which */ \ /* the reply slot carries. The query namespace is the context's (plan 4.3). */ \ - X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot) \ + X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot, kWaitReply) \ /* glQueryCounter(GL_TIMESTAMP) - QueryCounterTimestamp, a one-shot stamp into a query */ \ /* object, NOT a begin/end pair. Kind carries GL_TIMESTAMP. */ \ - X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone) \ + X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \ /* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \ /* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \ - X(FenceWaitServer, MGPFenceWait, kScreen, kNone) \ + X(FenceWaitServer, MGPFenceWait, kScreen, kNone, kWaitNone) \ /* ---- APPENDED BY P5b (MG_Remote/CONTRACT-P5B.md): five GLFunctionsTable verbs the ---- */ \ /* ---- class-C census measured as first blockers and that no existing row could carry. ---- */ \ /* ---- Opcodes 72..76. Every one reaches WireVerbSink, none has an MGPipeApply* entry ---- */ \ @@ -232,18 +271,18 @@ /* ---- directly, as today. The payload comments in MGPipeTypes.h say why each exists. ---- */ \ /* glBindImageTexture - Espryt syncs the unit's binding to the driver AT THE CALL */ \ /* (SyncImageTextureBinding), so the validate-time set_shader_images cannot stand in. */ \ - X(BindShaderImage, MGPImageBind, kCtxVerb, kNone) \ + X(BindShaderImage, MGPImageBind, kCtxVerb, kNone, kWaitNone) \ /* glPatchParameteri(GL_PATCH_VERTICES) - Espryt pushes glPatchParameteri at the call. */ \ - X(PatchParameter, MGPPatchParameter, kCtxVerb, kNone) \ + X(PatchParameter, MGPPatchParameter, kCtxVerb, kNone, kWaitNone) \ /* glBindTransformFeedback - the driver object rebind and the capture-binding shadow */ \ /* invalidation happen at the call; the XFB namespace has no wire lifetime. */ \ - X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone) \ + X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitApplied) \ /* glShaderStorageBlockBinding - the block NAME is the coordinate all three sides share */ \ /* (BackendObject.h), and a name is bytes: kHasBlob, staged whole in SEG_STAGE, NUL included. */ \ - X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \ + X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob, kWaitApplied) \ /* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \ /* which resource_copy_region (resource -> resource) cannot express. */ \ - X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone) \ + X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone, kWaitApplied) \ /* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \ /* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \ /* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \ @@ -255,14 +294,14 @@ /* against every verb that follows. ContextSerial is the client context's make-current */ \ /* serial at the edge - ASSERTED against the session's, never dispatched on (P5c: one */ \ /* context per session). */ \ - X(ApplierReset, MGPApplierReset, kScreen, kNone) \ + X(ApplierReset, MGPApplierReset, kScreen, kNone, kWaitApplied) \ /* A frontend state object died. The record carries the HANDLE (and kind) the client's own */ \ /* allocator resolves for it - nothing is emitted for an object that never crossed (the */ \ /* server never saw it, so there is no twin to kill) - and the sink releases the kind's */ \ /* twin table by handle. The framebuffer family's FIRST wire delete opcode: its death used */ \ /* to hop to the apply thread through a stack-struct mailbox and a lifetime-id probe of */ \ /* the client's allocator, both rule-E surfaces. */ \ - X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone) \ + X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone, kWaitNone) \ /* ---- P5c rv (CONTRACT-P5C.md §5.3), opcode 79: the residual-value record. One POD that ---- */ \ /* ---- carries every value-class BARRIER_PULLED field left after the set_* catalogue: the ---- */ \ /* ---- two texture-unit counters, the 15 per-target touched-buffer-binding counts and the ---- */ \ @@ -270,7 +309,20 @@ /* ---- (whole-record hash suppression, D11's shape), ACTIVE ONLY with an active transport ---- */ \ /* ---- (PipeFill.cpp gates both the emission and the residual-fill skip on it): under ---- */ \ /* ---- monolith there is no producer and the fields keep being pulled, byte for byte (G1). ---- */ \ - X(SetContextValues, MGPContextValues, kCtxState, kNone) + X(SetContextValues, MGPContextValues, kCtxState, kNone, kWaitNone) \ + /* ---- P5e (MG_Remote/CONTRACT-P5E.md §1), opcode 80: the post-link binding record. ------ */ \ + /* ---- THREE POST-LINK MUTABLE REFLECTION FIELDS LIVE INSIDE THE FRONTEND'S LinkArtifacts ---- */ \ + /* ---- and therefore inside the archive create_shader_state carries - but glUniformBlock- ---- */ \ + /* ---- Binding, glUniform1i on a sampler and glShaderStorageBlockBinding all move one ---- */ \ + /* ---- AFTER the link. So an archive snapshot alone answers a draw with the bindings the ---- */ \ + /* ---- program was LINKED with rather than the ones it is BOUND with, and the server's ---- */ \ + /* ---- twin rebuilds against the wrong uniform-block points. Three tails, three index ---- */ \ + /* ---- spaces (dense block index, sparse uniform location, and a NAME span for the storage ---- */ \ + /* ---- override map, which is name-keyed by design). Emitted BEFORE create_shader_state at ---- */ \ + /* ---- the same validate point: a rebuild inside the verb must already see the bindings. ---- */ \ + /* ---- kWaitNone: nothing about it is read back, and under run-ahead a kCtxState row that ---- */ \ + /* ---- names only handles and values is exactly what rule F admits. ---- */ \ + X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone) // clang-format on // Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index ebe2df50..ca5d77c3 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -93,7 +93,7 @@ #define MGP_FIELDS_MGPProgramDesc(F) \ F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \ - F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection) + F(PointSizeDemoted) F(EnableSpirvValidation) F(LinkStatus) F(Spirv) F(Reflection) // P4a, ID-12 / esprytobj DV-5: Pad0 became Uint16 TextureTarget. Same trip wire as // MGPFramebufferState's Target below - PADDING_MEMBER_RE only excludes a member still NAMED @@ -145,6 +145,19 @@ #define MGP_FIELDS_MGPStreamOutputTargets(F) \ F(Count) F(Generation) F(ContentHash) +// P5e (MG_Remote/CONTRACT-P5E.md §1): set_program_bindings and its two tail element types. The +// third tail's element embeds an MGHostSpan, which has a list of its own below, so the +// comparator sees the NAME SPAN's four members rather than memcmp'ing a struct with a pointer +// and a pad word in it - the exact false-difference MGPSamplerDesc's blob used to produce. +#define MGP_FIELDS_MGPProgramBindings(F) \ + F(Cso) F(Signature) F(BlockBindingCount) F(SamplerUnitCount) F(StorageOverrideCount) + +#define MGP_FIELDS_MGPProgramSamplerUnit(F) \ + F(Location) F(Unit) + +#define MGP_FIELDS_MGPProgramStorageOverride(F) \ + F(Name) F(Binding) + #define MGP_FIELDS_MGPGlobalConstants(F) \ F(ShaderCso) F(Version) F(Blob) @@ -394,6 +407,7 @@ P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \ P(MGPCopyFromFramebuffer) \ P(MGPApplierReset) P(MGPContextValues) \ + P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \ P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \ P(StencilFaceState) \ P(DynamicBackendParameters) P(MGHostSpan) \ diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index efa9fa4e..10fabc8c 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -29,7 +29,7 @@ struct MGPipeScreen { void (*ApplierReset)(const MGPApplierReset* payload); }; -// context: 67 calls. A null entry means the backend does not implement this +// context: 68 calls. A null entry means the backend does not implement this // call and the frontend keeps its own path (plan B section 4.1). struct MGPipeContext { void (*QueryCreate)(const MGPQueryDesc* payload); @@ -99,11 +99,12 @@ struct MGPipeContext { void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload); void (*ObjectDeath)(const MGPHandleOnly* payload); void (*SetContextValues)(const MGPContextValues* payload); + void (*SetProgramBindings)(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount); }; inline constexpr SizeT kMGPipeScreenCallCount = 12; -inline constexpr SizeT kMGPipeContextCallCount = 67; -inline constexpr SizeT kMGPipeCallCount = 79; +inline constexpr SizeT kMGPipeContextCallCount = 68; +inline constexpr SizeT kMGPipeCallCount = 80; // A table that is not exactly its call count of function pointers has grown a // member that no generator knows about. diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 0139b3a7..643b78d1 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -332,3 +332,7 @@ inline void MGP_ObjectDeath(const MGPHandleOnly* payload) { inline void MGP_SetContextValues(const MGPContextValues* payload) { gMGPipeContext.SetContextValues(payload); } + +inline void MGP_SetProgramBindings(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount) { + gMGPipeContext.SetProgramBindings(payload, varTail, varTailCount); +} diff --git a/MobileGL/MG_Pipe/generated/PipeVerify.inc b/MobileGL/MG_Pipe/generated/PipeVerify.inc index dd9ce0fb..d11fbb5c 100644 --- a/MobileGL/MG_Pipe/generated/PipeVerify.inc +++ b/MobileGL/MG_Pipe/generated/PipeVerify.inc @@ -114,6 +114,9 @@ inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockB inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField); inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField); inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField); +inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField); +inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField); +inline Bool MGPipeVerify(const MGPProgramStorageOverride& a, const MGPProgramStorageOverride& b, const char** outField); inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField); inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField); inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField); @@ -265,6 +268,12 @@ struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; @@ -678,6 +687,21 @@ inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, c return true; } +inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField) { + MGP_FIELDS_MGPProgramBindings(MGP_VERIFY_FIELD) + return true; +} + +inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField) { + MGP_FIELDS_MGPProgramSamplerUnit(MGP_VERIFY_FIELD) + return true; +} + +inline Bool MGPipeVerify(const MGPProgramStorageOverride& a, const MGPProgramStorageOverride& b, const char** outField) { + MGP_FIELDS_MGPProgramStorageOverride(MGP_VERIFY_FIELD) + return true; +} + inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) { MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD) return true; @@ -725,4 +749,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind #undef MGP_VERIFY_FIELD -inline constexpr SizeT kMGPipeVerifiedPayloadCount = 79; +inline constexpr SizeT kMGPipeVerifiedPayloadCount = 82; diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index 6f14039c..d1bb7246 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -149,7 +149,8 @@ enum class MGPWireOp : Uint16 { ApplierReset = 77, ObjectDeath = 78, SetContextValues = 79, - kOpCount = 80, + SetProgramBindings = 80, + kOpCount = 81, }; // THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is @@ -250,6 +251,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast(MGPWireOp::kOpCount) /* 77 ApplierReset */ static_cast(kNone), /* 78 ObjectDeath */ static_cast(kNone), /* 79 SetContextValues */ static_cast(kNone), + /* 80 SetProgramBindings */ static_cast(kVarTail | kHostSpan), }; static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_cast(MGPWireOp::kOpCount), @@ -349,6 +351,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast(MGPWireOp::kOpCount /* 77 ApplierReset */ static_cast(kScreen), /* 78 ObjectDeath */ static_cast(kCtxObject), /* 79 SetContextValues */ static_cast(kCtxState), + /* 80 SetProgramBindings */ static_cast(kCtxState), }; static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -361,6 +364,126 @@ inline constexpr MGPipeCallClass MGPipeCallClassFor(MGPWireOp op) { : kCallClassCount; } +// P5e (MG_Remote/CONTRACT-P5E.md 2.2): THE WAIT CLASS OF EVERY OPCODE, indexed the same way. +// Index 0 is kWaitClassCount - never a real class - for the flags table's reason: the one +// caller that can pass an opcode this catalogue never produced is a decoder holding bytes off +// a stream, and it must reach its own Fatal{ProtocolCorruption} rather than a wait decision. +// +// This is the STATIC HALF of MGPipeBarriered(op, payload, applierState), the predicate the emit +// table and the sink compute identically. The other half is payload-derived (an open +// transform-feedback span, a draw carrying client vertex arrays) and lives in that function, +// because it depends on state a table indexed by opcode cannot see. +inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount)] = { + /* 0 */ static_cast(kWaitClassCount), + + /* 1 GetCaps */ static_cast(kWaitReply), + /* 2 ResourceCreate */ static_cast(kWaitReply), + /* 3 ResourceRespecify */ static_cast(kWaitReply), + /* 4 ResourceDestroy */ static_cast(kWaitNone), + /* 5 MapPersistent */ static_cast(kWaitReply), + /* 6 UnmapPersistent */ static_cast(kWaitNone), + /* 7 FenceCreate */ static_cast(kWaitNone), + /* 8 FenceStatus */ static_cast(kWaitReply), + /* 9 FenceWait */ static_cast(kWaitReply), + /* 10 FenceDestroy */ static_cast(kWaitNone), + /* 11 QueryCreate */ static_cast(kWaitNone), + /* 12 QueryBegin */ static_cast(kWaitNone), + /* 13 QueryEnd */ static_cast(kWaitNone), + /* 14 QueryAvailable */ static_cast(kWaitReply), + /* 15 QueryResult */ static_cast(kWaitReply), + /* 16 QueryDestroy */ static_cast(kWaitNone), + /* 17 CreateRenderState */ static_cast(kWaitNone), + /* 18 BindRenderState */ static_cast(kWaitNone), + /* 19 DeleteRenderState */ static_cast(kWaitNone), + /* 20 CreateVertexElements */ static_cast(kWaitNone), + /* 21 BindVertexElements */ static_cast(kWaitNone), + /* 22 DeleteVertexElements */ static_cast(kWaitNone), + /* 23 CreateSamplerState */ static_cast(kWaitNone), + /* 24 DeleteSamplerState */ static_cast(kWaitNone), + /* 25 CreateSamplerView */ static_cast(kWaitNone), + /* 26 DeleteSamplerView */ static_cast(kWaitNone), + /* 27 CreateShaderState */ static_cast(kWaitNone), + /* 28 BindShaderState */ static_cast(kWaitNone), + /* 29 DeleteShaderState */ static_cast(kWaitNone), + /* 30 SetDynamicState */ static_cast(kWaitNone), + /* 31 SetFramebufferState */ static_cast(kWaitNone), + /* 32 SetVertexBuffers */ static_cast(kWaitNone), + /* 33 SetIndexBuffer */ static_cast(kWaitNone), + /* 34 SetIndirectBuffers */ static_cast(kWaitNone), + /* 35 SetSamplerViews */ static_cast(kWaitNone), + /* 36 BindSamplerStates */ static_cast(kWaitNone), + /* 37 SetShaderImages */ static_cast(kWaitNone), + /* 38 SetShaderBuffers */ static_cast(kWaitNone), + /* 39 SetStreamOutputTargets */ static_cast(kWaitNone), + /* 40 SetGlobalConstants */ static_cast(kWaitNone), + /* 41 SetVertexAttribDefaults */ static_cast(kWaitNone), + /* 42 SetPixelPackState */ static_cast(kWaitNone), + /* 43 SetPatchState */ static_cast(kWaitNone), + /* 44 SetDrawProgram */ static_cast(kWaitNone), + /* 45 SetDispatchProgram */ static_cast(kWaitNone), + /* 46 SetResidualValueState */ static_cast(kWaitNone), + /* 47 SetTextureParams */ static_cast(kWaitReply), + /* 48 ResourceSubData */ static_cast(kWaitReply), + /* 49 BufferSubDataResident */ static_cast(kWaitNone), + /* 50 ResourceSubDataComplete */ static_cast(kWaitNone), + /* 51 ResourceFlushRange */ static_cast(kWaitNone), + /* 52 ResourceReadback */ static_cast(kWaitReply), + /* 53 ResourceCopyRegion */ static_cast(kWaitNone), + /* 54 GenerateMipmap */ static_cast(kWaitApplied), + /* 55 GetTextureImage */ static_cast(kWaitReply), + /* 56 Blit */ static_cast(kWaitNone), + /* 57 Clear */ static_cast(kWaitNone), + /* 58 ReadPixels */ static_cast(kWaitReply), + /* 59 DrawVbo */ static_cast(kWaitNone), + /* 60 LaunchGrid */ static_cast(kWaitNone), + /* 61 MemoryBarrier */ static_cast(kWaitNone), + /* 62 BeginStreamOutput */ static_cast(kWaitApplied), + /* 63 EndStreamOutput */ static_cast(kWaitApplied), + /* 64 PauseStreamOutput */ static_cast(kWaitApplied), + /* 65 ResumeStreamOutput */ static_cast(kWaitApplied), + /* 66 Flush */ static_cast(kWaitNone), + /* 67 Present */ static_cast(kWaitPresent), + /* 68 SetSwapInterval */ static_cast(kWaitNone), + /* 69 QueryTimestamp */ static_cast(kWaitReply), + /* 70 QueryCounter */ static_cast(kWaitNone), + /* 71 FenceWaitServer */ static_cast(kWaitNone), + /* 72 BindShaderImage */ static_cast(kWaitNone), + /* 73 PatchParameter */ static_cast(kWaitNone), + /* 74 BindStreamOutput */ static_cast(kWaitApplied), + /* 75 SetStorageBlockBinding */ static_cast(kWaitApplied), + /* 76 CopyFramebufferToTexture*/ static_cast(kWaitApplied), + /* 77 ApplierReset */ static_cast(kWaitApplied), + /* 78 ObjectDeath */ static_cast(kWaitNone), + /* 79 SetContextValues */ static_cast(kWaitNone), + /* 80 SetProgramBindings */ static_cast(kWaitNone), +}; +static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == + static_cast(MGPWireOp::kOpCount), + "the wait-class table and the opcode space disagree"); + +inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) { + const SizeT index = static_cast(op); + return index < static_cast(MGPWireOp::kOpCount) + ? static_cast(kMGPipeWaitClasses[index]) + : kWaitClassCount; +} + +// THE COLUMN'S OWN INVARIANT, stated in C++ as well as in the generator: a reply-slot row +// waits for its reply and a waiting row owns a slot. gen_pipe.py refuses a catalogue where the +// two disagree, and this is the same statement where a reader of the generated table will look +// for it - a kReplySlot row that did not wait would run on and read a slot nobody had posted +// into, and a kWaitReply row with no slot would block on an answer nobody posts. +static_assert(MGPipeWaitClassFor(MGPWireOp::kInvalid) == kWaitClassCount, + "opcode 0 is not a call and has no wait class"); +static_assert(MGPipeWaitClassFor(MGPWireOp::Present) == kWaitPresent, + "present is paced by the credit, not by a wait after the publish"); +static_assert(MGPipeWaitClassFor(MGPWireOp::DrawVbo) == kWaitNone, + "the steady draw path is what P5e exists to stop waiting on"); +static_assert(MGPipeWaitClassFor(MGPWireOp::ApplierReset) == kWaitApplied, + "a make-current is a fresh server and every verb after it must see the reset"); +static_assert(MGPipeWaitClassFor(MGPWireOp::ReadPixels) == kWaitReply, + "a readback's answer is not derivable (R-5)"); + // Spot checks the generator states about its own output, so that a catalogue edit that // silently drops a flag is a build break here and not a wrong decode six packages away. static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast(kNone), @@ -1031,6 +1154,14 @@ static_assert(sizeof(MGPWireRec_SetContextValues) == ((sizeof(MGPWireRecHeader) + sizeof(MGPContextValues) + 7u) & ~SizeT(7u)), "MGPWireRec_SetContextValues gained padding; the wire format moved"); +struct alignas(8) MGPWireRec_SetProgramBindings { + MGPWireRecHeader Header; + MGPProgramBindings Payload; +}; +static_assert(sizeof(MGPWireRec_SetProgramBindings) == + ((sizeof(MGPWireRecHeader) + sizeof(MGPProgramBindings) + 7u) & ~SizeT(7u)), + "MGPWireRec_SetProgramBindings gained padding; the wire format moved"); + [[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) { MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call, static_cast(size), static_cast(remaining)); @@ -1317,6 +1448,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, case MGPWireOp::SetContextValues: MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetContextValues, "SetContextValues"); break; + case MGPWireOp::SetProgramBindings: + MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetProgramBindings, "SetProgramBindings"); + break; case MGPWireOp::kInvalid: case MGPWireOp::kOpCount: default: diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index fabec6c9..342b3e6e 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -242,7 +242,8 @@ namespace MobileGL::MG_Remote::Wire { X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \ X(ApplierReset, MGPApplierReset) \ X(ObjectDeath, MGPHandleOnly) \ - X(SetContextValues, MGPContextValues) + X(SetContextValues, MGPContextValues) \ + X(SetProgramBindings, MGPProgramBindings) namespace { @@ -602,6 +603,7 @@ namespace MobileGL::MG_Remote::Wire { // believed, and the bound is the header's own 32-bit Size field. Uint64 tail0 = 0; Uint64 tail1 = 0; + Uint64 tail2 = 0; // P5e: set_program_bindings is the one three-tail row Uint32 tails = 0; switch (op) { case MGPWireOp::SetVertexBuffers: { @@ -667,6 +669,47 @@ namespace MobileGL::MG_Remote::Wire { tails = 2; break; } + case MGPWireOp::SetProgramBindings: { + // THREE TAILS, THREE COUNTS, THREE INDEX SPACES (P5e, + // MG_Remote/CONTRACT-P5E.md §1): Int32[BlockBindingCount] in GL uniform-block index + // order, MGPProgramSamplerUnit[SamplerUnitCount] ascending by Location, and + // MGPProgramStorageOverride[StorageOverrideCount] whose Name is a host span staged + // whole - the override map is name-keyed by design. + // + // EACH COUNT IS REFUSED AGAINST ITS DECLARED BOUND, never truncated: a sampler + // array past 256 locations is a program this record cannot describe, and silently + // describing 256 of them would put uniforms on the wrong units with no marker + // anywhere. The counts come off the wire, so every one is bounded before it is + // multiplied. + const auto& p = *static_cast(payload); + if (p.BlockBindingCount > kMGPipeMaxProgramBlockBindings) { + WireProtocolFatalAt("SetProgramBindings.BlockBindingCount", p.BlockBindingCount, + kMGPipeMaxProgramBlockBindings); + } + if (p.SamplerUnitCount > kMGPipeMaxProgramSamplerUnits) { + WireProtocolFatalAt("SetProgramBindings.SamplerUnitCount", p.SamplerUnitCount, + kMGPipeMaxProgramSamplerUnits); + } + if (p.StorageOverrideCount > kMGPipeMaxProgramStorageOverrides) { + WireProtocolFatalAt("SetProgramBindings.StorageOverrideCount", + p.StorageOverrideCount, kMGPipeMaxProgramStorageOverrides); + } + tail0 = TailBytesFor(p.BlockBindingCount, sizeof(Int32)); + tail1 = TailBytesFor(p.SamplerUnitCount, sizeof(MGPProgramSamplerUnit)); + tail2 = TailBytesFor(p.StorageOverrideCount, sizeof(MGPProgramStorageOverride)); + // ALWAYS THREE, even when a count is 0. The tails are positional and the third one's + // offset is derived from the first two, so a record that declared "one tail" when + // only the block bindings were present would put the same bytes at a different + // offset than a record that declared three - and the two halves of the wire would + // disagree about where the overrides start. An empty tail is zero bytes at an + // 8-aligned offset, which costs nothing. + tails = 3; + // NOT SecondTailIsHostSpans: the spans are MEMBERS of the third tail's elements, + // not a bare MGHostSpan array, so the encoder's blanket honesty pass would read 40 + // bytes of {span, binding, pad} as one span and a quarter of garbage. The arm in + // ApplyChecked walks the elements and checks each Name itself. + break; + } case MGPWireOp::SetVertexAttribDefaults: { // TWO DECLARANTS THAT MUST AGREE (contract table 1 row 15): Count and // popcount(Mask). Nothing checks this today; a disagreement is a wire fault that @@ -749,6 +792,15 @@ namespace MobileGL::MG_Remote::Wire { out.TailBytes[1] = tail1; out.TotalBytes = out.TailOffset[1] + tail1; } + // P5e: the third tail, realigned to 8 for the second one's reason - set_program_bindings' + // first tail is Int32[] and its elements are four bytes, so the array behind it would + // otherwise start at a 4-aligned offset and every MGHostSpan in the third would be + // misaligned on a host that cares. + if (tails >= 3) { + out.TailOffset[2] = Align8(out.TailOffset[1] + tail1); + out.TailBytes[2] = tail2; + out.TotalBytes = out.TailOffset[2] + tail2; + } out.TailCount = tails; out.TotalBytes = Align8(out.TotalBytes); if (out.TotalBytes > kMaxRecordBytesOnTheWire) { @@ -1435,12 +1487,14 @@ namespace MobileGL::MG_Remote::Wire { // record declaring Count = 4000 while carrying 8 bytes to its own arithmetic. if (size != layout.TotalBytes) { MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and " - "its own count fields describe %llu bytes (fixed payload %llu + tails %llu/%llu)", + "its own count fields describe %llu bytes (fixed payload %llu + tails " + "%llu/%llu/%llu)", WireOpName(op), static_cast(size), static_cast(layout.TotalBytes), static_cast(layout.PayloadBytes), static_cast(layout.TailBytes[0]), - static_cast(layout.TailBytes[1])); + static_cast(layout.TailBytes[1]), + static_cast(layout.TailBytes[2])); std::abort(); } @@ -1806,6 +1860,33 @@ namespace MobileGL::MG_Remote::Wire { // Same as above: no applier, off the reduced path, both tails validated. return false; + case MGPWireOp::SetProgramBindings: { + // P5e, opcode 80 (MG_Remote/CONTRACT-P5E.md §1). NO APPLIER ENTRY POINT EXISTS YET + // and c0e does not invent one: the record's consumer is the program twin's + // rebuild, which package pg writes. What the record needs NOW and cannot get later + // is its shape - the three declared counts against their bounds and the three-tail + // arithmetic, both of which MGPipeWireRecordLayout above has already run - plus the + // honesty of every name span it carries. + // + // THE SPANS ARE WALKED HERE RATHER THAN BY THE ENCODER'S BLANKET PASS because they + // are MEMBERS of the third tail's 40-byte elements, not a bare MGHostSpan array: + // SecondTailIsHostSpans reads a tail AS spans, and doing that to {span, binding, + // pad} would read one span and a quarter of garbage. All four arms of + // CheckHostSpanIsHonest run, the segment-range one included, so an override name + // pointing outside SEG_STAGE is caught on the way in rather than at the rebuild. + if (layout.TailCount == 3 && layout.TailBytes[2] != 0 && m_segments != nullptr) { + const auto* overrides = tailAt(2); + const Uint64 count = layout.TailBytes[2] / sizeof(MGPProgramStorageOverride); + for (Uint64 i = 0; i < count; ++i) { + MGPProgramStorageOverride entry{}; + std::memcpy(&entry, overrides + i * sizeof(MGPProgramStorageOverride), + sizeof(MGPProgramStorageOverride)); + CheckHostSpanIsHonest(entry.Name, *m_segments); + } + } + return false; + } + case MGPWireOp::SetGlobalConstants: { const auto& rec = *static_cast(payload); // The length cross-check is the applier's (PipeApply.cpp:2784, against the diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 5c3eb9c2..05d42005 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -171,10 +171,19 @@ namespace MobileGL::MG_Remote::Wire { // holds a pointer and two Uint64s. P5 emits no host span at all, so this rule costs // nothing now and is stated now because the phase that arms kDrawHasUserIndices would // otherwise have to discover it as a misaligned load on a device. + // THREE TAILS AND NOT TWO SINCE P5e. set_program_bindings (opcode 80, + // MG_Remote/CONTRACT-P5E.md §1) carries the uniform-block bindings, the sampler units and + // the storage-override map as three arrays in three different index spaces, so the array + // that used to be exactly "the two-tail rows plus draw_vbo's conditional second" grew by + // one. Every loop over TailCount below already reads the count rather than the literal 2; + // what changed is the storage and the assembly. + inline constexpr Uint32 kMaxWireRecordTails = 3; + struct WireRecordLayout { Uint64 PayloadBytes = 0; // sizeof the op's payload struct - Uint64 TailOffset[2] = {0, 0}; // from the START of the record, header included - Uint64 TailBytes[2] = {0, 0}; + Uint64 TailOffset[kMaxWireRecordTails] = {0, 0, 0}; // from the START of the record, + // header included + Uint64 TailBytes[kMaxWireRecordTails] = {0, 0, 0}; Uint32 TailCount = 0; Uint64 TotalBytes = 0; // header + payload + gaps + tails, rounded up to 8 // P5b: WHAT THE SECOND TAIL IS. SetShaderBuffers' second tail is MGHostSpan[HostSpanCount] @@ -194,9 +203,9 @@ namespace MobileGL::MG_Remote::Wire { // The catalogue's own spelling of an opcode, for a Fatal line. Out of range is "". const char* WireOpName(MG_Pipe::MGPWireOp op); - // One tail array. Two of the 71 rows carry two (SetShaderBuffers, SetStreamOutputTargets) - // and DrawVbo carries a conditional second one, which is why EncodeRecord's one-tail form - // could not stay the only one. + // One tail array. Two of the rows carry two (SetShaderBuffers, SetStreamOutputTargets), + // DrawVbo carries a conditional second one and P5e's SetProgramBindings carries three, + // which is why EncodeRecord's one-tail form could not stay the only one. struct WireTail { const void* Bytes = nullptr; Uint64 Size = 0; @@ -234,8 +243,10 @@ namespace MobileGL::MG_Remote::Wire { Uint64 EncodeRecord(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, const void* varTail = nullptr, Uint64 varTailBytes = 0); - // The same call for the three rows that carry TWO tails. The one-tail form above is - // this one with tailCount <= 1; nothing is duplicated between them. + // The same call for the rows that carry more than one tail - two for + // set_shader_buffers, set_stream_output_targets and draw_vbo's conditional second, + // three for P5e's set_program_bindings. The one-tail form above is this one with + // tailCount <= 1; nothing is duplicated between them. // // The tails a caller hands over are CROSS-CHECKED against the layout the payload // itself declares (MGPipeWireRecordLayout): a caller whose Count says 4000 while its diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 3d1e9d26..359dd707 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -65,8 +65,8 @@ TEST(PipeCatalogue, FrontendNeverTakesAnApplierAddress) { namespace { // Counting expansions of the catalogue. The Class parameter is a real enumerator, so a // per-class count is a constant expression too. -#define MGP_COUNT_ONE(Name, Payload, Class, Flags) +1 -#define MGP_COUNT_CLASS(Name, Payload, Class, Flags) +((Class) == countedClass ? 1 : 0) +#define MGP_COUNT_ONE(Name, Payload, Class, Flags, Wait) +1 +#define MGP_COUNT_CLASS(Name, Payload, Class, Flags, Wait) +((Class) == countedClass ? 1 : 0) constexpr SizeT kExpandedCallCount = 0 MGP_CALL_LIST(MGP_COUNT_ONE); @@ -77,13 +77,32 @@ namespace { // Every payload named in the catalogue must be a memcpy-able POD, and so must every // payload the verify comparator knows about. -#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags) \ +#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags, Wait) \ static_assert(std::is_trivially_copyable_v, #Name "'s payload " #Payload " is not trivially copyable"); MGP_CALL_LIST(MGP_ASSERT_CALL_PAYLOAD_POD) #define MGP_ASSERT_VERIFY_PAYLOAD_POD(Payload) \ static_assert(std::is_trivially_copyable_v, #Payload " is not trivially copyable"); MGP_VERIFY_PAYLOAD_LIST(MGP_ASSERT_VERIFY_PAYLOAD_POD) + + // The catalogue's own spelling of an opcode, for a failure message. The wire codec has one + // of these and MG_Pipe may not reach it (MG_Remote is above this layer), so the table is + // expanded from the catalogue here rather than a name being duplicated per EXPECT. +#define MGP_CATALOGUE_NAME_ROW(Name, Payload, Class, Flags, Wait) #Name, + constexpr const char* kCatalogueOpNames[] = { + "", + MGP_CALL_LIST(MGP_CATALOGUE_NAME_ROW) + }; +#undef MGP_CATALOGUE_NAME_ROW + static_assert(sizeof(kCatalogueOpNames) / sizeof(kCatalogueOpNames[0]) == + static_cast(MGPWireOp::kOpCount), + "the diagnostic name table and the opcode space disagree"); + + const char* WireOpNameForDiag(MGPWireOp op) { + const SizeT index = static_cast(op); + return index < static_cast(MGPWireOp::kOpCount) ? kCatalogueOpNames[index] + : ""; + } } // namespace // The handle is the whole object model. Eight bytes, a register pair, no padding. @@ -127,11 +146,12 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { // The per-class counts PipeCalls.def documents in its header. // kScreen is 11 + P5c's applier_reset (MG_Remote/CONTRACT-P5C.md §5.1); kCtxObject is // 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode. - // kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record. + // kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's + // set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record. EXPECT_EQ(ClassCount(), 12u); EXPECT_EQ(ClassCount(), 8u); EXPECT_EQ(ClassCount(), 13u); - EXPECT_EQ(ClassCount(), 18u); + EXPECT_EQ(ClassCount(), 19u); EXPECT_EQ(ClassCount(), 10u); // 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image, // patch_parameter, bind_stream_output, set_storage_block_binding, @@ -142,9 +162,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { // A row nobody has migrated is null - which is exactly what "this subsystem has not been // migrated, keep pulling" means (plan B section 4.1). // -// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 41 ROWS WITH -// NO MGPipeApply* ENTRY POINT (79 - the 38 that have one; the number was 34 at P5, 39 after -// P5b's five sink-only verbs, and rv's set_context_values grew BOTH sides of the difference): +// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 42 ROWS WITH +// NO MGPipeApply* ENTRY POINT (80 - the 38 that have one; the number was 34 at P5, 39 after +// P5b's five sink-only verbs, rv's set_context_values grew BOTH sides of the difference, and +// P5e's set_program_bindings grew the null side alone): // the other 38 have an applier, R-17 installs adapters over them, // and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through // the thunks, so a null would mean "call through a null pointer". The number is asserted rather @@ -161,7 +182,7 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) { const void* const* context = reinterpret_cast(&gMGPipeContext); #if MOBILEGL_PIPE_PUSH MGPipeInstallMonolithTables(); - // The 41 rows with no MGPipeApply* entry point are still null, and null still means "this + // The 42 rows with no MGPipeApply* entry point are still null, and null still means "this // subsystem has not been migrated, keep pulling". Named rather than counted, because the // count is the other case's job and two cases asserting the same number would both go red // for one change. P5c's two control records are among them by design (CONTRACT-P5C.md §5: @@ -177,6 +198,10 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) { EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr); EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr); EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr); + // P5e's opcode 80 joins them: the row is catalogued, its codec layout is written and its + // tails are validated, but there is no MGPipeApplySetProgramBindings and no route - package + // pg installs both. A null here is the contract, not an omission. + EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr); #else // A pull build compiles no applier and no routing, so the pre-migration statement is the // whole truth there and this case is the one that says so. @@ -251,6 +276,9 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) { // notice's mailbox are the producers, byte for byte as before (G1/G2). EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr); EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr); + // P5e's set_program_bindings (CONTRACT-P5E.md §1): catalogued with a null route until pg + // installs the applier entry point and the adapter beside it. + EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr); #else // A pull build compiles no applier and no routing, so the pre-migration statement is still // the whole truth there. @@ -584,7 +612,14 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { // - fixed POD, no blob, no tail, no reply - with an MGPipeApply* entry point, which the two // control records deliberately do not have. EXPECT_EQ(static_cast(MGPWireOp::SetContextValues), 79); - EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 80); + // P5e (MG_Remote/CONTRACT-P5E.md §1) appended ONE more, by the same rule: opcode 80, and + // nothing before it moved. set_program_bindings is a kCtxState row with THREE tails and a + // host span in the third; it lands with a null route and a sink that refuses it by name, + // exactly as set_shader_buffers has sat catalogued-and-dead since P4a. + EXPECT_EQ(static_cast(MGPWireOp::SetProgramBindings), 80); + EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 81); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetProgramBindings), + static_cast(kVarTail | kHostSpan)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast(kNone)); @@ -618,9 +653,44 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { EXPECT_EQ(sizeof(MGPContextValues::TouchedBufferBindingPointCount), 60u); EXPECT_EQ(sizeof(MGPAttribValue), 56u); EXPECT_EQ(sizeof(MGPVertexAttribDefaults), 8u); + // P5e's payloads (CONTRACT-P5E.md §1), pinned at runtime like every other MGP_ASSERT_POD. + // + // MGPProgramDesc GREW 192 -> 200, and that is the phase's one wire-format widening: the + // descriptor's four trailing Uint8s end exactly at offset 24 and MGPBlobRef is 8-aligned, + // so there was no spare byte for LinkStatus and the draft's "in the descriptor's existing + // pad" was wrong. Pinned here, by value, because a record whose size moved silently is a + // protocol break no other test would see - and because THIS number is the red-once the + // phase names: change MGPProgramBindings' or the descriptor's size and this case fails. + EXPECT_EQ(sizeof(MGPProgramDesc), 200u); + EXPECT_EQ(sizeof(MGPProgramBindings), 32u); + EXPECT_EQ(sizeof(MGPProgramSamplerUnit), 8u); + EXPECT_EQ(sizeof(MGPProgramStorageOverride), 40u); + // The three declared tail bounds. A program past one of them is a COUNTED REFUSAL and never + // a truncation, so the numbers are the contract rather than an implementation detail. + EXPECT_EQ(kMGPipeMaxProgramBlockBindings, 64u); + EXPECT_EQ(kMGPipeMaxProgramSamplerUnits, 256u); + EXPECT_EQ(kMGPipeMaxProgramStorageOverrides, 64u); + // LinkStatus is a real byte and a zeroed descriptor means "not linked", which is what makes + // the server's decline the safe default. + MGPProgramDesc program{}; + EXPECT_EQ(program.LinkStatus, 0u); + // set_shader_buffers' capacity, pinned on the wire side; MG_Impl/Pipe/PipeFill.cpp is the + // one translation unit that also sees BufferState.h's constant and pins them together. + EXPECT_EQ(kMGPipeMaxBufferBindingPoints, 84u); // 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); + // P5e's draw flag: the next free bit, and disjoint from all five before it. + EXPECT_EQ(static_cast(kDrawClientArrays), 1u << 6); + EXPECT_EQ(static_cast(kDrawClientArrays) & + static_cast(kDrawHasUserIndices | kDrawPrimitiveRestart | + kDrawIndicesAreClient | kDrawHasIndexRange | + kDrawHasXfbCount | kDrawIsIndirect), + 0u); + // ... and it still fits the one byte MGPDrawInfo::Flags is, which is why the record did not + // grow: MGPDrawInfo stays 56 bytes. + EXPECT_EQ(sizeof(MGPDrawInfo), 56u); + EXPECT_LE(static_cast(kDrawClientArrays), 0xFFu); // The clear discriminants have ONE spelling now, and Whole is 0 so a zeroed record is a // whole-framebuffer clear. EXPECT_EQ(kMGPipeClearKindWhole, 0u); @@ -628,6 +698,107 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { EXPECT_EQ(kMGPipeClearValueClassUint + 1, kMGPipeClearValueClassCount); } +// ---- P5e: the wait-class column (MG_Remote/CONTRACT-P5E.md §2.2) -------------------------- +// +// THE POINT OF THIS CASE IS THAT IT DOES NOT READ PipeCalls.def. Every other statement about +// the column - the generated table, the two gates in gen_pipe.py, the static_asserts in +// PipeWire.inc - is derived from the def, so all of them move together when a row is edited. +// This case re-states CONTRACT-P5E §2.2's table BY NAME, in the contract's own grouping, so a +// row silently moved from kWaitApplied to kWaitNone has to be argued in two places or it goes +// red here. That is the difference between a table and a contract. +// +// It matters because the column is not decoration: on a run-ahead server kWaitNone is a promise +// that the record's apply reads NOTHING of the client's (rule F), and a row demoted to it by +// accident is a torn read that renders wrong rather than aborting. +TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) { + // kWaitPresent: present alone, paced by the credit taken BEFORE the encode (§2.4). + EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::Present), kWaitPresent); + + // kWaitReply: every row whose answer is not derivable (R-5). Named, all fourteen, because + // the flag-to-class agreement is what keeps a waiter from hanging on a slot nobody posts. + const MGPWireOp replyRows[] = { + MGPWireOp::GetCaps, MGPWireOp::ResourceCreate, MGPWireOp::ResourceRespecify, + MGPWireOp::MapPersistent, MGPWireOp::FenceStatus, MGPWireOp::FenceWait, + MGPWireOp::QueryAvailable, MGPWireOp::QueryResult, MGPWireOp::QueryTimestamp, + MGPWireOp::SetTextureParams, MGPWireOp::ResourceSubData, MGPWireOp::ResourceReadback, + MGPWireOp::GetTextureImage, MGPWireOp::ReadPixels, + }; + for (const MGPWireOp op : replyRows) { + EXPECT_EQ(MGPipeWaitClassFor(op), kWaitReply) << WireOpNameForDiag(op); + EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast(kReplySlot), 0u) + << WireOpNameForDiag(op); + } + + // kWaitApplied: the rows whose apply STILL reads a BARRIER_PULLED row or probes the client + // allocator, so the client parks in its own wait and CONTRACT-P5C's semantics hold for them + // unchanged - their pulls count into rsp and their probes are legal inside a scope. Two of + // them are trailing items and are expected to leave this list in a later package: + // generate_mipmap the moment tx2 resolves the texture from VerbMipRes instead of the active + // unit, and set_storage_block_binding the moment pg resolves it through + // MGPStorageBlockBinding::ShaderCso. When they do, THIS list is what has to be edited. + const MGPWireOp appliedRows[] = { + MGPWireOp::ApplierReset, MGPWireOp::GenerateMipmap, + MGPWireOp::SetStorageBlockBinding, MGPWireOp::BeginStreamOutput, + MGPWireOp::EndStreamOutput, MGPWireOp::PauseStreamOutput, + MGPWireOp::ResumeStreamOutput, MGPWireOp::BindStreamOutput, + MGPWireOp::CopyFramebufferToTexture, + }; + for (const MGPWireOp op : appliedRows) { + EXPECT_EQ(MGPipeWaitClassFor(op), kWaitApplied) << WireOpNameForDiag(op); + } + + // kWaitNone: the steady draw path and every state/CSO/object row without a reply. Named for + // the rows the phase exists for, and then counted for the rest, so a new row cannot join + // the set unnoticed. + const MGPWireOp noneRows[] = { + MGPWireOp::DrawVbo, MGPWireOp::LaunchGrid, MGPWireOp::Clear, + MGPWireOp::Blit, MGPWireOp::MemoryBarrier, MGPWireOp::Flush, + MGPWireOp::SetSwapInterval, MGPWireOp::BindShaderImage, MGPWireOp::PatchParameter, + MGPWireOp::ObjectDeath, MGPWireOp::ResourceDestroy, MGPWireOp::UnmapPersistent, + MGPWireOp::FenceCreate, MGPWireOp::FenceDestroy, MGPWireOp::FenceWaitServer, + MGPWireOp::QueryCreate, MGPWireOp::QueryBegin, MGPWireOp::QueryEnd, + MGPWireOp::QueryDestroy, MGPWireOp::QueryCounter, MGPWireOp::SetContextValues, + MGPWireOp::SetProgramBindings, MGPWireOp::SetShaderBuffers, + MGPWireOp::SetVertexBuffers, MGPWireOp::SetFramebufferState, + MGPWireOp::CreateShaderState, MGPWireOp::SetDrawProgram, + }; + for (const MGPWireOp op : noneRows) { + EXPECT_EQ(MGPipeWaitClassFor(op), kWaitNone) << WireOpNameForDiag(op); + } + + // The partition, by count. 14 + 1 + 9 = 24 rows wait; every other row of the catalogue does + // not. A row that changed class moves two of these numbers at once. + SizeT reply = 0, applied = 0, present = 0, none = 0, other = 0; + for (SizeT i = 1; i < static_cast(MGPWireOp::kOpCount); ++i) { + switch (MGPipeWaitClassFor(static_cast(i))) { + case kWaitReply: ++reply; break; + case kWaitApplied: ++applied; break; + case kWaitPresent: ++present; break; + case kWaitNone: ++none; break; + default: ++other; break; + } + } + EXPECT_EQ(reply, 14u); + EXPECT_EQ(applied, 9u); + EXPECT_EQ(present, 1u); + EXPECT_EQ(none, static_cast(kMGPipeCallCount) - 24u); + EXPECT_EQ(other, 0u) << "a row carries the kWaitClassCount terminator as its class"; + + // And the reply half of the partition BOTH WAYS, over the whole catalogue: exactly the rows + // that own a slot wait for one. gen_pipe.py refuses a catalogue where they disagree; this + // is the same statement where a reader of the table will look for it. + for (SizeT i = 1; i < static_cast(MGPWireOp::kOpCount); ++i) { + const auto op = static_cast(i); + const Bool ownsSlot = (MGPipeCallFlagsFor(op) & static_cast(kReplySlot)) != 0; + EXPECT_EQ(ownsSlot, MGPipeWaitClassFor(op) == kWaitReply) << WireOpNameForDiag(op); + } + + // Opcode 0 is not a call and has no class, for the flags table's reason: a decoder holding + // a byte off a corrupt stream must reach its own Fatal rather than a wait decision. + EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kInvalid), kWaitClassCount); + EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kOpCount), kWaitClassCount); +} + // A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted" // is reported as "not applied" rather than "fatal". TEST(PipeCatalogue, ApplierAcceptsAWellFormedRecord) { @@ -817,8 +988,12 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) { // each with its own field list, so the comparator sees every one of them: 77. P5c appended // applier_reset's MGPApplierReset (CONTRACT-P5C.md §5.1) - object_death reuses // MGPHandleOnly, which has had a list since P0 - and rv added set_context_values' - // MGPContextValues (§5.3): 79. - EXPECT_EQ(kMGPipeVerifiedPayloadCount, 79u); + // MGPContextValues (§5.3): 79. P5e appended set_program_bindings' MGPProgramBindings AND + // the two TAIL ELEMENT types beside it, MGPProgramSamplerUnit and MGPProgramStorageOverride + // (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange + // and MGPVertexAttribWire are: the comparator has to see INTO an element whose members + // include an MGHostSpan and a pad word, or it would memcmp the padding: 82. + EXPECT_EQ(kMGPipeVerifiedPayloadCount, 82u); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); @@ -925,7 +1100,7 @@ TEST(PipeCatalogue, BufferRangeCarriesNoInlineHostSpan) { // The call still declares the span it may carry, so the transport lays the tail out. Uint32 flags = 0; -#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags) \ +#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags, Wait) \ if (std::strcmp(#Name, "SetShaderBuffers") == 0) flags = static_cast(Flags); MGP_CALL_LIST(MGP_FLAGS_OF_SET_SHADER_BUFFERS) #undef MGP_FLAGS_OF_SET_SHADER_BUFFERS @@ -1048,7 +1223,7 @@ TEST(PipeCatalogue, VertexWireViewsAreFlatAndCarryIsLongSeparately) { // inherits this pin rather than the guess. TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) { Uint32 flags = 0; -#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags) \ +#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags, Wait) \ if (std::strcmp(#Name, "ResourceRespecify") == 0) flags = static_cast(Flags); MGP_CALL_LIST(MGP_FLAGS_OF_RESOURCE_RESPECIFY) #undef MGP_FLAGS_OF_RESOURCE_RESPECIFY @@ -1056,7 +1231,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) { // And it is the ONLY call that carries it: a second one would be a second decision, and // this predicate answers for exactly one call. Uint32 ackingCalls = 0; -#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags) \ +#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags, Wait) \ if ((static_cast(Flags) & static_cast(kNeedsAck)) != 0) ++ackingCalls; MGP_CALL_LIST(MGP_COUNT_ACKING_CALLS) #undef MGP_COUNT_ACKING_CALLS @@ -1071,7 +1246,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) { // ROWS, whose applier entry points return a Bool the client acts on destructively and which // carried no flag because in monolith that answer is a direct call's return value. Uint32 replySlotCalls = 0; -#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags) \ +#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags, Wait) \ if ((static_cast(Flags) & static_cast(kReplySlot)) != 0) ++replySlotCalls; MGP_CALL_LIST(MGP_COUNT_REPLY_SLOT_CALLS) #undef MGP_COUNT_REPLY_SLOT_CALLS @@ -1082,7 +1257,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) { // (PipeApply.h:820, :868, :897, :1023); map_persistent's void* is the fifth answer and was // already declared. Uint32 acceptanceWithSlot = 0; -#define MGP_COUNT_ACCEPTANCE_ROWS(Name, Payload, Class, Flags) \ +#define MGP_COUNT_ACCEPTANCE_ROWS(Name, Payload, Class, Flags, Wait) \ if ((std::strcmp(#Name, "ResourceCreate") == 0 || std::strcmp(#Name, "ResourceRespecify") == 0 || \ std::strcmp(#Name, "ResourceSubData") == 0 || std::strcmp(#Name, "SetTextureParams") == 0) && \ (static_cast(Flags) & static_cast(kReplySlot)) != 0) { \ diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index af7dca4a..4db7c34e 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -136,12 +136,15 @@ def read(path): class Call(object): - def __init__(self, index, name, payload, cls, flags): + def __init__(self, index, name, payload, cls, flags, wait="kWaitNone"): self.Index = index # 1-based; this is the wire opcode self.Name = name self.Payload = payload self.Class = cls self.Flags = flags + # P5e: PipeCalls.def's fifth column. Defaulted so the self-test's canned Calls - + # which exercise the FLAG gates and say nothing about waiting - stay one line. + self.Wait = wait @property def IsScreen(self): @@ -176,7 +179,8 @@ class Call(object): return ", ".join(params), ", ".join(args) -CALL_RE = re.compile(r"^\s*X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*([\w|]+?)\s*\)\s*\\?\s*$") +CALL_RE = re.compile(r"^\s*X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*([\w|]+?)\s*," + r"\s*(\w+)\s*\)\s*\\?\s*$") def parse_calls(): @@ -195,7 +199,7 @@ def parse_calls(): match = CALL_RE.match(line) if match: calls.append(Call(len(calls) + 1, match.group(1), match.group(2), match.group(3), - match.group(4).split("|"))) + match.group(4).split("|"), match.group(5))) # The macro ends at the first line without a continuation backslash. if not line.rstrip().endswith("\\"): inside = False @@ -564,6 +568,60 @@ def check_call_flags_are_known(calls): % (call.Name, "|".join(f for f in call.Flags if f != "kNone"))) +WAIT_ENUM_RE = re.compile(r"enum\s+MGPipeWaitClass\s*:\s*Uint8\s*\{(.*?)\}\s*;", re.S) +WAIT_MEMBER_RE = re.compile(r"^\s*(kWait\w+)\s*(?:=\s*\d+\s*)?,", re.M) + + +def parse_wait_classes(): + """The MGPipeWaitClass enumerators, READ OUT OF MGPipe.h, for parse_call_flags' reason: + this list is what the generated kMGPipeWaitClasses[] table spells into C++, and a copy + here would go stale in exactly the case that matters. kWaitClassCount is the terminator and + is NOT a class a row may carry - a row that named it would generate a table entry the + lookup uses to mean 'this opcode never came from the catalogue'.""" + text = read(os.path.join(PIPE_DIR, "MGPipe.h")) + body = WAIT_ENUM_RE.search(text) + if not body: + sys.exit("MGPipe.h: enum MGPipeWaitClass : Uint8 is missing or has changed shape") + names = WAIT_MEMBER_RE.findall(body.group(1)) + if "kWaitClassCount" not in names: + sys.exit("MGPipe.h: enum MGPipeWaitClass has no kWaitClassCount terminator") + classes = [n for n in names if n != "kWaitClassCount"] + if not classes: + sys.exit("MGPipe.h: enum MGPipeWaitClass declares no wait classes") + return classes + + +KNOWN_WAIT_CLASSES = tuple(parse_wait_classes()) + + +def check_wait_classes_are_known(calls): + """P5e (CONTRACT-P5E.md §2.2). Two gates over PipeCalls.def's fifth column, both in --check + as well, because the column is what the client's wait rule and the sink's barriered + predicate are BOTH computed from - a typo here is a wrong wait on a live wire rather than a + compile error in one consumer. + + 1. The token must be an MGPipeWaitClass enumerator, and never the kWaitClassCount + terminator, which the generated lookup uses for "not from this catalogue". + 2. kReplySlot and kWaitReply must agree, BOTH WAYS. A reply-slot row that did not wait + would be a waiter that hangs - the client would run on and read a slot nobody had + posted into yet; a waiting row with no slot would block on an answer nobody posts. + This is the same class of defect R-13.4 found between the flag column and the payloads, + and it is stated here for the same reason: nothing else has both facts in one place.""" + for call in calls: + if call.Wait not in KNOWN_WAIT_CLASSES: + sys.exit("PipeCalls.def: %s carries wait class %s, which is not an MGPipeWaitClass " + "enumerator (%s)" % (call.Name, call.Wait, ", ".join(KNOWN_WAIT_CLASSES))) + owns_slot = "kReplySlot" in call.Flags + waits_reply = call.Wait == "kWaitReply" + if owns_slot and not waits_reply: + sys.exit("PipeCalls.def: %s owns a reply slot but its wait class is %s; a " + "kReplySlot row must be kWaitReply or the client reads a slot nobody " + "posted into" % (call.Name, call.Wait)) + if waits_reply and not owns_slot: + sys.exit("PipeCalls.def: %s is kWaitReply but carries no kReplySlot; it would " + "block on an answer nobody posts" % call.Name) + + def parse_coverage(): text = read(os.path.join(PIPE_DIR, "Coverage.def")) accessors = [] @@ -800,6 +858,48 @@ inline constexpr MGPipeCallClass MGPipeCallClassFor(MGPWireOp op) { : kCallClassCount; } +// P5e (MG_Remote/CONTRACT-P5E.md 2.2): THE WAIT CLASS OF EVERY OPCODE, indexed the same way. +// Index 0 is kWaitClassCount - never a real class - for the flags table's reason: the one +// caller that can pass an opcode this catalogue never produced is a decoder holding bytes off +// a stream, and it must reach its own Fatal{ProtocolCorruption} rather than a wait decision. +// +// This is the STATIC HALF of MGPipeBarriered(op, payload, applierState), the predicate the emit +// table and the sink compute identically. The other half is payload-derived (an open +// transform-feedback span, a draw carrying client vertex arrays) and lives in that function, +// because it depends on state a table indexed by opcode cannot see. +inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount)] = { + /* 0 */ static_cast(kWaitClassCount), +""") + for call in calls: + out.append(" /* %2d %-24s*/ static_cast(%s)," % (call.Index, call.Name, call.Wait)) + out.append("};") + out.append("static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==") + out.append(" static_cast(MGPWireOp::kOpCount),") + out.append(" \"the wait-class table and the opcode space disagree\");") + out.append(""" +inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) { + const SizeT index = static_cast(op); + return index < static_cast(MGPWireOp::kOpCount) + ? static_cast(kMGPipeWaitClasses[index]) + : kWaitClassCount; +} + +// THE COLUMN'S OWN INVARIANT, stated in C++ as well as in the generator: a reply-slot row +// waits for its reply and a waiting row owns a slot. gen_pipe.py refuses a catalogue where the +// two disagree, and this is the same statement where a reader of the generated table will look +// for it - a kReplySlot row that did not wait would run on and read a slot nobody had posted +// into, and a kWaitReply row with no slot would block on an answer nobody posts. +static_assert(MGPipeWaitClassFor(MGPWireOp::kInvalid) == kWaitClassCount, + "opcode 0 is not a call and has no wait class"); +static_assert(MGPipeWaitClassFor(MGPWireOp::Present) == kWaitPresent, + "present is paced by the credit, not by a wait after the publish"); +static_assert(MGPipeWaitClassFor(MGPWireOp::DrawVbo) == kWaitNone, + "the steady draw path is what P5e exists to stop waiting on"); +static_assert(MGPipeWaitClassFor(MGPWireOp::ApplierReset) == kWaitApplied, + "a make-current is a fresh server and every verb after it must see the reset"); +static_assert(MGPipeWaitClassFor(MGPWireOp::ReadPixels) == kWaitReply, + "a readback's answer is not derivable (R-5)"); + // Spot checks the generator states about its own output, so that a catalogue edit that // silently drops a flag is a build break here and not a wrong decode six packages away. static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast(kNone), @@ -1515,6 +1615,23 @@ def self_test(accessors): "combines kNone with", lambda: check_call_flags_are_known([flag_kNone]))) + # P5e's two gates over the fifth column (CONTRACT-P5E.md §2.2). The column decides whether + # a live client waits after publishing, so a typo or a disagreement with the flag column is + # a wrong wait on the wire rather than a compile error in one consumer - which is exactly + # the shape R-13.4 found between the flag column and the payloads. + wait_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitApplyed") + wait_slot_no_wait = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kReplySlot"], "kWaitNone") + wait_no_slot = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitReply") + controls.append(("wait class that is not an MGPipeWaitClass enumerator", + "which is not an MGPipeWaitClass enumerator", + lambda: check_wait_classes_are_known([wait_typo]))) + controls.append(("a kReplySlot row that does not wait for its reply", + "owns a reply slot but its wait class is", + lambda: check_wait_classes_are_known([wait_slot_no_wait]))) + controls.append(("a kWaitReply row with no reply slot", + "but carries no kReplySlot", + lambda: check_wait_classes_are_known([wait_no_slot]))) + # R-16 meta-control (codex review finding 9; verified by execution in # p5-results/wave1-codex-verify.md §9). The exact perturbation there replaced the # flag-typo control's callback with `sys.exit("unrelated parser failure")` and, @@ -1540,8 +1657,9 @@ def self_test(accessors): # The positive control: the canned struct's exact list passes, and the parser sees the # padding member as padding and the function as not a member. check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct]) - # ... and the real catalogue's real flags pass the same gate. + # ... and the real catalogue's real flags and wait classes pass the same gates. check_call_flags_are_known(calls_for_control) + check_wait_classes_are_known(calls_for_control) if trips == 0: sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything") if trips != len(controls): @@ -1561,6 +1679,7 @@ def main(): calls = parse_calls() payloads = parse_verify_payloads() check_call_flags_are_known(calls) + check_wait_classes_are_known(calls) check_call_payloads_have_field_lists(calls, payloads) check_field_lists_cover_struct_members(parse_field_lists(), payloads) accessors, deltas, sticky, emitted = parse_coverage()