[MG_Pipe, MG_Remote, MG_Test] (Disaggregated): P5e c0e - the wait-class column and opcode 80

- PipeCalls.def gains a FIFTH COLUMN, WaitClass, on every row, and gen_pipe.py generates
  MGPipeWaitClassFor(op) beside kMGPipeCallFlags. It is a VALUE and not a new op: opcode
  order and the flag column are untouched, and the point of generating it is that the emit
  table and the sink compute the barriered predicate's static half from ONE table instead
  of two copies that agree until they do not.
- Two new generator gates, each with its own --self-test negative control: a token that is
  not an MGPipeWaitClass enumerator, and - both ways - a kReplySlot row that does not wait
  for its reply or a kWaitReply row that owns no slot. The first would be a waiter that
  hangs, the second a block on an answer nobody posts. Twelve controls trip now, up from
  nine.
- set_program_bindings appended as opcode 80 (kCtxState, kVarTail|kHostSpan): the three
  POST-LINK mutable reflection fields, which live inside the LinkArtifacts the archive
  snapshots and all move after the link, so no existing row can carry them. It lands with
  a NULL ROUTE and a sink arm that validates and declines, exactly as set_shader_buffers
  has sat catalogued-and-dead since P4a.
- The codec's record layout grows to THREE tails for it, with each declared count refused
  against its bound rather than truncated, and the third tail's name spans walked through
  CheckHostSpanIsHonest in the decode arm - they are members of a 40-byte element, so the
  encoder's blanket second-tail pass would read one span and a quarter of garbage.
- PipeCatalogueTest: the new sizes (MGPProgramDesc 200), the opcode-80 and null-route pins,
  the draw flag, and a wait-class case that re-states CONTRACT-P5E section 2.2's table BY
  NAME rather than reading the def - every other statement about the column is derived
  from the def and moves with it, so this is the second place a demoted row has to be
  argued. The ABI fingerprint moves with MGPWireOp::kOpCount, which the handshake case
  already pins.
This commit is contained in:
2026-09-18 06:26:14 -04:00
parent 7568161eb4
commit e99796d6ec
10 changed files with 736 additions and 121 deletions
+136 -84
View File
@@ -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"):
+15 -1
View File
@@ -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) \
+4 -3
View File
@@ -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.
@@ -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);
}
+25 -1
View File
@@ -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<MGPApplierReset> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramSamplerUnit> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramStorageOverride> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<PixelStoreParameters> : 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;
+135 -1
View File
@@ -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<SizeT>(MGPWireOp::kOpCount)
/* 77 ApplierReset */ static_cast<Uint32>(kNone),
/* 78 ObjectDeath */ static_cast<Uint32>(kNone),
/* 79 SetContextValues */ static_cast<Uint32>(kNone),
/* 80 SetProgramBindings */ static_cast<Uint32>(kVarTail | kHostSpan),
};
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -349,6 +351,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 77 ApplierReset */ static_cast<Uint8>(kScreen),
/* 78 ObjectDeath */ static_cast<Uint8>(kCtxObject),
/* 79 SetContextValues */ static_cast<Uint8>(kCtxState),
/* 80 SetProgramBindings */ static_cast<Uint8>(kCtxState),
};
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
static_cast<SizeT>(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<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kWaitClassCount),
/* 1 GetCaps */ static_cast<Uint8>(kWaitReply),
/* 2 ResourceCreate */ static_cast<Uint8>(kWaitReply),
/* 3 ResourceRespecify */ static_cast<Uint8>(kWaitReply),
/* 4 ResourceDestroy */ static_cast<Uint8>(kWaitNone),
/* 5 MapPersistent */ static_cast<Uint8>(kWaitReply),
/* 6 UnmapPersistent */ static_cast<Uint8>(kWaitNone),
/* 7 FenceCreate */ static_cast<Uint8>(kWaitNone),
/* 8 FenceStatus */ static_cast<Uint8>(kWaitReply),
/* 9 FenceWait */ static_cast<Uint8>(kWaitReply),
/* 10 FenceDestroy */ static_cast<Uint8>(kWaitNone),
/* 11 QueryCreate */ static_cast<Uint8>(kWaitNone),
/* 12 QueryBegin */ static_cast<Uint8>(kWaitNone),
/* 13 QueryEnd */ static_cast<Uint8>(kWaitNone),
/* 14 QueryAvailable */ static_cast<Uint8>(kWaitReply),
/* 15 QueryResult */ static_cast<Uint8>(kWaitReply),
/* 16 QueryDestroy */ static_cast<Uint8>(kWaitNone),
/* 17 CreateRenderState */ static_cast<Uint8>(kWaitNone),
/* 18 BindRenderState */ static_cast<Uint8>(kWaitNone),
/* 19 DeleteRenderState */ static_cast<Uint8>(kWaitNone),
/* 20 CreateVertexElements */ static_cast<Uint8>(kWaitNone),
/* 21 BindVertexElements */ static_cast<Uint8>(kWaitNone),
/* 22 DeleteVertexElements */ static_cast<Uint8>(kWaitNone),
/* 23 CreateSamplerState */ static_cast<Uint8>(kWaitNone),
/* 24 DeleteSamplerState */ static_cast<Uint8>(kWaitNone),
/* 25 CreateSamplerView */ static_cast<Uint8>(kWaitNone),
/* 26 DeleteSamplerView */ static_cast<Uint8>(kWaitNone),
/* 27 CreateShaderState */ static_cast<Uint8>(kWaitNone),
/* 28 BindShaderState */ static_cast<Uint8>(kWaitNone),
/* 29 DeleteShaderState */ static_cast<Uint8>(kWaitNone),
/* 30 SetDynamicState */ static_cast<Uint8>(kWaitNone),
/* 31 SetFramebufferState */ static_cast<Uint8>(kWaitNone),
/* 32 SetVertexBuffers */ static_cast<Uint8>(kWaitNone),
/* 33 SetIndexBuffer */ static_cast<Uint8>(kWaitNone),
/* 34 SetIndirectBuffers */ static_cast<Uint8>(kWaitNone),
/* 35 SetSamplerViews */ static_cast<Uint8>(kWaitNone),
/* 36 BindSamplerStates */ static_cast<Uint8>(kWaitNone),
/* 37 SetShaderImages */ static_cast<Uint8>(kWaitNone),
/* 38 SetShaderBuffers */ static_cast<Uint8>(kWaitNone),
/* 39 SetStreamOutputTargets */ static_cast<Uint8>(kWaitNone),
/* 40 SetGlobalConstants */ static_cast<Uint8>(kWaitNone),
/* 41 SetVertexAttribDefaults */ static_cast<Uint8>(kWaitNone),
/* 42 SetPixelPackState */ static_cast<Uint8>(kWaitNone),
/* 43 SetPatchState */ static_cast<Uint8>(kWaitNone),
/* 44 SetDrawProgram */ static_cast<Uint8>(kWaitNone),
/* 45 SetDispatchProgram */ static_cast<Uint8>(kWaitNone),
/* 46 SetResidualValueState */ static_cast<Uint8>(kWaitNone),
/* 47 SetTextureParams */ static_cast<Uint8>(kWaitReply),
/* 48 ResourceSubData */ static_cast<Uint8>(kWaitReply),
/* 49 BufferSubDataResident */ static_cast<Uint8>(kWaitNone),
/* 50 ResourceSubDataComplete */ static_cast<Uint8>(kWaitNone),
/* 51 ResourceFlushRange */ static_cast<Uint8>(kWaitNone),
/* 52 ResourceReadback */ static_cast<Uint8>(kWaitReply),
/* 53 ResourceCopyRegion */ static_cast<Uint8>(kWaitNone),
/* 54 GenerateMipmap */ static_cast<Uint8>(kWaitApplied),
/* 55 GetTextureImage */ static_cast<Uint8>(kWaitReply),
/* 56 Blit */ static_cast<Uint8>(kWaitNone),
/* 57 Clear */ static_cast<Uint8>(kWaitNone),
/* 58 ReadPixels */ static_cast<Uint8>(kWaitReply),
/* 59 DrawVbo */ static_cast<Uint8>(kWaitNone),
/* 60 LaunchGrid */ static_cast<Uint8>(kWaitNone),
/* 61 MemoryBarrier */ static_cast<Uint8>(kWaitNone),
/* 62 BeginStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 63 EndStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 64 PauseStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 65 ResumeStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 66 Flush */ static_cast<Uint8>(kWaitNone),
/* 67 Present */ static_cast<Uint8>(kWaitPresent),
/* 68 SetSwapInterval */ static_cast<Uint8>(kWaitNone),
/* 69 QueryTimestamp */ static_cast<Uint8>(kWaitReply),
/* 70 QueryCounter */ static_cast<Uint8>(kWaitNone),
/* 71 FenceWaitServer */ static_cast<Uint8>(kWaitNone),
/* 72 BindShaderImage */ static_cast<Uint8>(kWaitNone),
/* 73 PatchParameter */ static_cast<Uint8>(kWaitNone),
/* 74 BindStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 75 SetStorageBlockBinding */ static_cast<Uint8>(kWaitApplied),
/* 76 CopyFramebufferToTexture*/ static_cast<Uint8>(kWaitApplied),
/* 77 ApplierReset */ static_cast<Uint8>(kWaitApplied),
/* 78 ObjectDeath */ static_cast<Uint8>(kWaitNone),
/* 79 SetContextValues */ static_cast<Uint8>(kWaitNone),
/* 80 SetProgramBindings */ static_cast<Uint8>(kWaitNone),
};
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"the wait-class table and the opcode space disagree");
inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeWaitClass>(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<Uint32>(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<unsigned long long>(size), static_cast<unsigned long long>(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:
+84 -3
View File
@@ -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<const MGPProgramBindings*>(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<unsigned long long>(size),
static_cast<unsigned long long>(layout.TotalBytes),
static_cast<unsigned long long>(layout.PayloadBytes),
static_cast<unsigned long long>(layout.TailBytes[0]),
static_cast<unsigned long long>(layout.TailBytes[1]));
static_cast<unsigned long long>(layout.TailBytes[1]),
static_cast<unsigned long long>(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<const MGPGlobalConstants*>(payload);
// The length cross-check is the applier's (PipeApply.cpp:2784, against the
+18 -7
View File
@@ -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 "<opcode>".
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
+192 -17
View File
@@ -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<Payload>, #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>, #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[] = {
"<kInvalid>",
MGP_CALL_LIST(MGP_CATALOGUE_NAME_ROW)
};
#undef MGP_CATALOGUE_NAME_ROW
static_assert(sizeof(kCatalogueOpNames) / sizeof(kCatalogueOpNames[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"the diagnostic name table and the opcode space disagree");
const char* WireOpNameForDiag(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kCatalogueOpNames[index]
: "<opcode out of range>";
}
} // 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<kScreen>(), 12u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 18u);
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
EXPECT_EQ(ClassCount<kCtxObject>(), 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<const void* const*>(&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<Uint16>(MGPWireOp::SetContextValues), 79);
EXPECT_EQ(static_cast<Uint16>(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<Uint16>(MGPWireOp::SetProgramBindings), 80);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 81);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetProgramBindings),
static_cast<Uint32>(kVarTail | kHostSpan));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(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<Uint32>(kDrawIsIndirect), 1u << 5);
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
// P5e's draw flag: the next free bit, and disjoint from all five before it.
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays), 1u << 6);
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays) &
static_cast<Uint32>(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<Uint32>(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<Uint32>(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<SizeT>(MGPWireOp::kOpCount); ++i) {
switch (MGPipeWaitClassFor(static_cast<MGPWireOp>(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<SizeT>(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<SizeT>(MGPWireOp::kOpCount); ++i) {
const auto op = static_cast<MGPWireOp>(i);
const Bool ownsSlot = (MGPipeCallFlagsFor(op) & static_cast<Uint32>(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<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::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<Uint32>(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<Uint32>(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<Uint32>(Flags) & static_cast<Uint32>(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<Uint32>(Flags) & static_cast<Uint32>(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<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) { \
+123 -4
View File
@@ -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<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kWaitClassCount),
""")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint8>(%s)," % (call.Index, call.Name, call.Wait))
out.append("};")
out.append("static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==")
out.append(" static_cast<SizeT>(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<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeWaitClass>(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<Uint32>(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()