diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 1bc17800..485899b5 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -322,12 +322,16 @@ namespace MobileGL::MG_Config { // 0 - the only shipped value until the migration lands - is "pull everything", // i.e. exactly today's behaviour, and is the default of a PULL build, where the // knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so - // far (MG_Pipe::kMGPipeSubsystemsMigratedAtP2), so MOBILEGL_PIPE_PUSH=0 in the - // environment is the all-pull control. Accepts decimal or 0x-prefixed hex, and - // operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h owns them): + // far (MG_Pipe::kMGPipeSubsystemsMigratedAtP3a), so MOBILEGL_PIPE_PUSH=0 in the + // environment is the all-pull control and 0x7f (kMGPipeSubsystemsMigratedAtP2) is + // the "P2 only" control P3a's A/B is run against. Accepts decimal or 0x-prefixed + // hex, and operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h + // owns them): // 0x01 render state (create/bind_render_state + set_dynamic_state) // 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults // 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input + // 0x80 resources (the resource_* family: the seven BufferBackendOps hooks) + // 0x100 vertex input (vertex elements / vertex buffers / index buffer) // 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of // CSOs, so every pipeline-version change mints a fresh CSO and the map is // never probed. The negative control the CSO design is measured against. diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 7d66491d..2a2dd67f 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -250,8 +250,10 @@ namespace MobileGL::MG_ConfigLoader { #if MOBILEGL_PIPE_PUSH // A push build with the knob unset runs every subsystem migrated so far, so the // shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the - // environment is the all-subsystems-pull control that reproduces P1 exactly. - features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP2); + // environment is the all-subsystems-pull control that reproduces P1 exactly, and + // kMGPipeSubsystemsMigratedAtP2 (0x7f) is the phase-by-phase control - P3a's two + // subsystems off, everything P2 landed still on. + features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP3a); #else // Meaningless in a pull build: there is nothing to push. Config.h documents 0 as // "pull everything" and that stays literally true. diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index 620d8385..759848ae 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -245,6 +245,46 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry.backend; } + // P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this + // header records against itself at the top of the file. + // + // The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's + // lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by + // the CLIENT, and under a real split neither the object nor its lifetime id exists on + // this side. This overload never touches the allocator at all. The handle ARRIVED, in + // the call's payload, already minted by the side that owns minting; all this does is + // index the slot, notice a generation that no longer matches (the slot was recycled, + // so the twin at it describes driver ids the new resource never made) and hand back + // the twin pointer. FindByHandle beside it is the same shape and already existed. + // + // No StatePtr, therefore no Entry::stateRef: the weak pointer is liveness for + // ForEachLive() and a handle-keyed entry has no frontend object to weakly hold. Such + // an entry is therefore invisible to ForEachLive, which is correct - the one direct + // iteration site walks texture twins, and it is not one of these tables. + // + // Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the + // announcement is the family's own destroy call, not the shared death notice, and the + // slot is freed by the CLIENT after that call returns. + // + // UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so + // an uninstantiated one costs nothing anywhere, and the backend package is what gives + // it its first caller. + BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) { + MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), + "GetOrCreate(handle) named the reserved null handle"); + if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin; + + // Same arming as the minting overload, and for the same reason: twin creation is + // the moment a driver-owned id starts needing a guarded destructor. + EnsureProcessTeardownSentinel(); + + Entry& entry = EntryAt(handle.Slot); + if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset(); + entry.Gen = handle.Gen; + entry.Live = true; + return entry.backend; + } + // Null when no live twin of this object exists. Unlike the registry's Find this NEVER // mutates the table, so the returned pointer survives any later Find on it; only a // GetOrCreate that grows the vector can move it, and callers that hold one across a diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index c80846d3..fa67dea8 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -631,6 +631,12 @@ namespace MobileGL::MG_Pipe { return kMGPipeSubsystemPatchState; case MGPipeFieldEmitter::SetVertexAttribDefaults: return kMGPipeSubsystemVertexAttribDefaults; + // P3a. bind_vertex_elements is the vertex-input family's only emitter row today + // (Coverage.def says why the other two candidates are not there); the resource + // family has none at all, because its calls are dispatched at the GL call that + // causes them rather than filled into a PipeInputs field. + case MGPipeFieldEmitter::BindVertexElements: + return kMGPipeSubsystemVertexInput; case MGPipeFieldEmitter::kNone: break; } @@ -662,10 +668,53 @@ namespace MobileGL::MG_Pipe { MGPipeSubsystemForDirty(MGPipeDirty::NewVertexAttribDefaults), "set_vertex_attrib_defaults and NEW_VERTEX_ATTRIB_DEFAULTS must name one subsystem"); + // P3a's pairing, in the two halves the contract commit can actually state. + // + // The four above compare the two maps directly, which is only possible once BOTH + // sides name the subsystem. MGPipeSubsystemForDirty is MG_Impl/Pipe/Tracker.h's and + // its bit 5 / 9 / 10 arms land with the client emitters, not here - so the direct + // form would fail at this commit for a reason that is not a defect. What is stated + // instead is exactly as strong in the direction that matters: + // + // (a) the emitter half names the vertex-input subsystem, so a later edit that moved + // it onto a different one fails here; + // (b) the two maps AGREE OR THE DIRTY HALF IS NOT MAPPED YET. The escape hatch is + // the not-yet-mapped case only: the moment Tracker.h maps NEW_VERTEX_ELEMENTS + // onto anything at all, this becomes the equality the four above are; + // (c) and while the dirty half is unmapped the subsystem is NOT in + // kMGPipeWiredSubsystems below, so no field can be skipped on the strength of a + // call nobody emits. (c) is what makes (b)'s hatch safe rather than convenient. + static_assert(SubsystemForEmitter(MGPipeFieldEmitter::BindVertexElements) == + kMGPipeSubsystemVertexInput, + "bind_vertex_elements must name the vertex-input subsystem"); + static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements) == 0 || + MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements) == + SubsystemForEmitter(MGPipeFieldEmitter::BindVertexElements), + "bind_vertex_elements and NEW_VERTEX_ELEMENTS must name one subsystem"); + static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers) == 0 || + MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers) == + kMGPipeSubsystemVertexInput, + "set_vertex_buffers and NEW_VERTEX_BUFFERS must name one subsystem"); + static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer) == 0 || + MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer) == + kMGPipeSubsystemVertexInput, + "set_index_buffer and NEW_INDEX_BUFFER must name one subsystem"); + // The two vertex views' capacity is one number on both sides of the boundary. This is + // the one translation unit that sees the frontend constant and the MG_Pipe one, so it + // is where they are pinned together; MGPipeTypes.h says so in place. + static_assert(kMGPipeMaxVertexAttribs == + MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, + "the MGPipe vertex-attribute capacity and the frontend's have drifted"); + // Which of those subsystems THIS BUILD actually emits for. It grows one commit at a // time, and a field whose emitter is not wired here keeps being pulled - so adding a // row to Coverage.def can never silently drop a field on the floor before the call // that carries it exists. + // + // P3a's two are DELIBERATELY ABSENT at the contract commit: the three emitters below + // are stubs that emit nothing and the applier's fourteen entry points are stubs that + // apply nothing, so wiring either bit here would retire a pull for a call that does + // not happen yet. The commit that gives the emitters their bodies adds them. constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState | kMGPipeSubsystemPixelPack | kMGPipeSubsystemPatchState | @@ -1044,6 +1093,35 @@ namespace MobileGL::MG_Pipe { } return payloadBytes; } + + // ---- P3a's three vertex-input emitters. STUBS AT THE CONTRACT COMMIT. ---- + // + // They exist here, and are called from the validate point below, for the same reason + // the applier's fourteen entry points exist as stubs: this file's enum-coupled block + // is the contract commit's and everything else in it belongs to the commit that + // fills the bodies in, so the two must not have to touch the same lines. What lands + // here is the SHAPE - three functions, in the emission order the design fixes + // (elements, then buffers, then index, after the four P2 emitters) - and the bodies + // replace `return 0` without moving a call site. + // + // THEY ARE UNREACHABLE, not merely empty: MGPipeSubsystemForDirty maps NEW_VERTEX_ + // ELEMENTS / _BUFFERS / _INDEX_BUFFER onto no subsystem yet, so `wants()` is false + // for all three whatever MOBILEGL_PIPE_PUSH says. Returning 0 keeps them out of the + // payload histogram, which must not gain a bucket for bytes nobody sent. + Uint64 EmitVertexElements(GLContext& ctx) { + (void)ctx; + return 0; + } + + Uint64 EmitVertexBuffers(GLContext& ctx) { + (void)ctx; + return 0; + } + + Uint64 EmitIndexBuffer(GLContext& ctx) { + (void)ctx; + return 0; + } } // namespace Uint64 MGPipeVertexAttribDefaultRepairCount() { return g_attribDefaultRepairs; } @@ -1127,6 +1205,21 @@ namespace MobileGL::MG_Pipe { payloadBytes += EmitVertexAttribDefaults(*ctx, tracker.FreshlyPrimed()); } + // P3a's vertex segment, in the order the design fixes: vertex elements, then the + // vertex buffers that fill them, then the index binding. All three still resolve to + // false today - their dirty bits map to no subsystem until the tracker's arms land - + // and all three emitters are stubs; the call sites are here so the commit that gives + // them bodies does not also have to edit the validate point. + if (wants(MGPipeDirty::NewVertexElements)) { + payloadBytes += EmitVertexElements(*ctx); + } + if (wants(MGPipeDirty::NewVertexBuffers)) { + payloadBytes += EmitVertexBuffers(*ctx); + } + if (wants(MGPipeDirty::NewIndexBuffer)) { + payloadBytes += EmitIndexBuffer(*ctx); + } + // ---- step 4: the residual fill, for what an emitted call did NOT supply ---- const Bool applierDerives = ApplierDerivesRenderStateFields(); for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) { diff --git a/MobileGL/MG_Pipe/Coverage.def b/MobileGL/MG_Pipe/Coverage.def index 4ee3ab58..51a19618 100644 --- a/MobileGL/MG_Pipe/Coverage.def +++ b/MobileGL/MG_Pipe/Coverage.def @@ -34,11 +34,26 @@ /* dead: no backend reads it since D21; kept for inventory row 594 */ \ X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \ X(GetBoundVertexArray, BindVertexElements) \ - /* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \ - /* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \ - /* inventory is re-vendored carrying the target argument (deferred out of P1: */ \ - /* the extractor lives in MobileGL-CS). Named for the plan's explicit */ \ - /* replacement of the DrawIndirect/Parameter pair. */ \ + /* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \ + /* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \ + /* target argument), but by supplying the target from the EMISSION SITE, which */ \ + /* knows it exactly. The split, target by target: */ \ + /* ArrayBuffer, and the per-attribute buffer of a VAO -> set_vertex_buffers */ \ + /* ElementArrayBuffer -> set_index_buffer */ \ + /* DrawIndirectBuffer, ParameterBuffer -> set_indirect_buffers */ \ + /* Uniform/ShaderStorage/AtomicCounter/TransformFeedback */ \ + /* -> set_shader_buffers */ \ + /* CopyRead/CopyWrite/PixelPack/PixelUnpack/Texture -> still pulled: the */ \ + /* transfer and pixel-store targets have no call of their own yet. */ \ + /* THE ROW STAYS ONE ROW, and that is structural rather than a shortcut: this */ \ + /* list IS the MGPipeInputField enum and the PipeInputs field set, and the */ \ + /* field is ONE array (m_bufferBindingSlot[kBufferTargetCount]) that a second */ \ + /* row of the same name could only duplicate. So the split lives here and in */ \ + /* the emitters, and the row keeps naming set_indirect_buffers for the plan's */ \ + /* explicit replacement of the DrawIndirect/Parameter pair. It is deliberately */ \ + /* NOT in the EMITTED list below: five targets above are still pulled, and a */ \ + /* row there says "the whole field is supplied", which for this field would be */ \ + /* the same half-truth GetPixelStoreParameters is kept out for. */ \ X(GetBufferBindingSlot, SetIndirectBuffers) \ X(GetBufferBindingPoint, SetShaderBuffers) \ X(GetBufferBindingPointCount, SetShaderBuffers) \ @@ -151,8 +166,25 @@ // nothing while its poison stamp said it was published, so neither the poison nor the verify // comparator could see it. Until the field is split, the whole of it keeps going through the // fill loop and the pack half is simply written twice. +// +// P3a ADDS ONE ROW, GetBoundVertexArray -> BindVertexElements, and it is the vertex-input +// family's only candidate: GetBufferBindingSlot is polymorphic over a target set P3a covers +// only part of (see its comment above) and GetCurrentVertexAttribute has been here since P2. +// The row is INERT until the vertex-input subsystem is wired - MG_Impl/Pipe/PipeFill.cpp's +// kMGPipeWiredSubsystems does not carry that bit at the contract commit, because the emitters +// beside it are still stubs - which is exactly the guard that lets a row land before the call +// that carries it exists. +// +// A NOTE FOR THE COMMIT THAT WIRES IT, because it is not visible from this file: the field is +// a shared pointer to the frontend VAO, and the vertex-input calls supply the CONFIGURATION +// (the applier's MGPipeVertexElementsRecord), not the object. So the row is shape-only in the +// same sense GetCurrentVertexAttribute's is, and it stays so until the backend's twin +// resolution reads the applier's BoundVertexElements instead of the object - at which point +// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately +// rather than silently by this row's presence. #define MGP_COVERAGE_EMITTED_LIST(X) \ X(GetBlendColor, SetDynamicState) \ + X(GetBoundVertexArray, BindVertexElements) \ X(GetBlendEquationIndexed, CreateRenderState) \ X(GetBlendFuncIndexed, CreateRenderState) \ X(GetClampReadColor, SetDynamicState) \ diff --git a/MobileGL/MG_Pipe/MGPipe.h b/MobileGL/MG_Pipe/MGPipe.h index b1caf7d5..4cb07e94 100644 --- a/MobileGL/MG_Pipe/MGPipe.h +++ b/MobileGL/MG_Pipe/MGPipe.h @@ -76,13 +76,23 @@ namespace MobileGL::MG_Pipe { inline constexpr Uint64 kMGPipeSubsystemResidualValues = 1ull << 4; inline constexpr Uint64 kMGPipeSubsystemEsprytSlots = 1ull << 5; // Track H, Espryt 0b inline constexpr Uint64 kMGPipeSubsystemMagmaVertexInput = 1ull << 6; // Track H, Magma subsystem 4 - // bits 7..62 reserved for the later phases, allocated in ROADMAP order. + // P3a's two. Resources is the seven BufferBackendOps hooks turned into the handle-shaped + // resource_* family; VertexInput is vertex elements, vertex buffers and the index buffer. + // They are separate bits because they are separate A/Bs: a buffer path that regressed and + // a vertex path that regressed are different findings, and clearing one must not disarm + // the other. + inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7; + inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8; + // bits 9..62 reserved for the later phases, allocated in ROADMAP order. // NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so // every pipeline-version change mints a fresh CSO and the map is never probed. This is // the negative control the whole CSO design is measured against (ROADMAP.md P2). inline constexpr Uint64 kMGPipeBehaviourNoCsoContentAddressing = 1ull << 63; - // The default of a push build with the knob unset (ConfigLoader.cpp). - inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6 + // The default of a push build with the knob unset (ConfigLoader.cpp). Each phase's + // constant STAYS, because it is the A/B control for the phase after it: P3a's + // "everything P2 had and nothing of mine" arm is spelled MOBILEGL_PIPE_PUSH=0x7f. + inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6 + inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8 // The catalogue itself. Only macros, so it is safe to expand inside the namespace, and // consumers (the unit test, later the transport) get MGP_CALL_LIST from this header. diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 708bd992..74138cad 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -353,6 +353,13 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPFramebufferState, 304); + // GL_MAX_VERTEX_ATTRIBS as MobileGL advertises it, on the MG_Pipe side of the boundary. + // It bounds the two declared counts of MGPVertexElements, the applier's two per-CSO + // arrays and the vertex-buffer set. It MUST equal VertexArrayObject::MAX_VERTEX_ATTRIBS; + // MG_Impl/Pipe/PipeFill.cpp is the one translation unit that sees both and carries the + // static_assert, because this header may not include a frontend one. + inline constexpr Uint32 kMGPipeMaxVertexAttribs = 32; + struct MGPVertexBuffer { MGPipeHandle Res; Uint64 Offset; @@ -366,9 +373,25 @@ namespace MobileGL::MG_Pipe { // Var-tail header: MGPVertexBuffer[Count] follows. struct MGPVertexBuffers { Uint32 Start, Count; + // The vertex-FETCH base instance these offsets are valid for (P3a, D-H1). It is DRAW + // state, not VAO state, and it is NOT the same thing as MGPDrawInfo::StartInstance: + // that one is the GL draw's baseInstance and feeds gl_BaseInstance, this one is the + // shift the fetch address of an instanced array needs when the device has no native + // base-instance support. The server decides whether to emulate it or let + // GL_EXT_base_instance do the work - emulation is server-owned - so the client sends + // the draw's raw value and never a pre-shifted offset. + // + // IT IS A ContentHash INPUT, and that is a requirement rather than a nicety: + // set_vertex_buffers is suppressed on an unchanged hash (MG_Impl/Pipe/ + // SetHashSuppressor.h's SetVertexBuffers slot), so a baseInstance that moved while + // the buffer set did not would be suppressed and the server would keep the previous + // shift. It rides ONE PER EMITTED SET rather than per entry: per entry the shift is + // redundant and lets a malformed record disagree with itself. + Uint32 BaseInstance; + Uint32 Pad0; Uint64 ContentHash; }; - MGP_ASSERT_POD(MGPVertexBuffers, 16); + MGP_ASSERT_POD(MGPVertexBuffers, 24); // An independent call, NOT a subset of the VAO configuration version (D5). struct MGPIndexBuffer { @@ -616,6 +639,22 @@ namespace MobileGL::MG_Pipe { } inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; } + // P3a, D-A5: the per-record half of resource_respecify's kNeedsAck. + // + // Flags are a PER-CALL static property and resource_respecify serves BOTH glBufferData + // and glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData + // in a world upload; only glBufferStorage is a real synchronous allocation and only it is + // allowed a synchronous ack. So kNeedsAck on the call means "records of this call MAY + // require an acknowledgement" and THIS predicate decides per record. In monolith the ack + // is ((void)0) - the applier is one function call away - and the transport wires the + // doorbell to this predicate when it lands. + // + // Immutable is exactly the right discriminator: it is set iff the store came from a + // glBufferStorage* entry point, which is the definition of the allowed case. + inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) { + return desc.Immutable != 0; + } + // The forward terminator for a server-initiated texture pull (section 7.1). May carry // zero regions - that is how a pull that needs nothing is answered. struct MGPSubDataComplete { diff --git a/MobileGL/MG_Pipe/MGPipeValueTypes.h b/MobileGL/MG_Pipe/MGPipeValueTypes.h index 888cb3ad..e2da5312 100644 --- a/MobileGL/MG_Pipe/MGPipeValueTypes.h +++ b/MobileGL/MG_Pipe/MGPipeValueTypes.h @@ -544,6 +544,61 @@ namespace MobileGL { }; } // namespace MG_State::GLState + // ---- P3a: the WIRE forms of the two views above (ARCHITECTURE.md section on vertex + // elements; brief D-G2). Neither VertexAttribute nor VertexBufferBindingPoint can travel + // as itself: both hold a SharedPtr, and a payload never contains a pointer. + // They live here rather than in MGPipeTypes.h so the structs they mirror are one screen + // away and a member added above has its wire twin in view; MGPipeTypes.h includes this + // header, so MG_Pipe sees them unqualified like every other value type. + // + // Both ride the create_vertex_elements BLOB, in ascending index order, attributes first: + // MGPVertexAttribWire[AttributeCount] then MGPVertexBindingPointWire[BindingPointCount], + // each count <= VertexArrayObject::MAX_VERTEX_ATTRIBS (32). The applier refuses a record + // whose declared counts do not match the blob's declared size. + + // The resolved flat attribute view. Buffer identity does NOT travel here - it travels in + // set_vertex_buffers, which is what keeps this record stable while buffers change under + // it. Stride is the RESOLVED distance and a surviving 0 can only have come from the + // binding model (see VertexAttribute::Stride above); collapsing it back into the element + // size is what made KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the + // buffer. Divisor is deliberately ABSENT: it is resolved per binding point and travels in + // MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads it. + // LegacyStride / LegacyPointer are likewise absent - they are the glGetVertexAttrib* + // query answers and stay client-side, because nothing but the query path reads them. + struct MGPVertexAttribWire { + Uint64 Offset; // 0 + Int32 Stride; // 8 + Uint32 Type; // 12 DataType + Uint8 Size; // 16 1..4; GL_BGRA keeps 4 + Uint8 Enabled; // 17 + Uint8 Normalized; // 18 + Uint8 IsInteger; // 19 + // CARRIED SEPARATELY from Type == Float64, and it has to be: VertexAttribFormat( + // GL_DOUBLE) also reads doubles from memory but asks for them converted to float, + // while VertexAttribLFormat keeps all 64 bits. The backend's fp64 narrowing and its + // Adreno disabled-attribute workaround both key on telling the two apart. + Uint8 IsLong; // 20 + Uint8 IsBgra; // 21 + Uint8 BindingIndex; // 22 which MGPVertexBuffer entry feeds it (< MAX_VERTEX_ATTRIBS) + Uint8 Pad0; // 23 + }; + + // The ARB_vertex_attrib_binding view. Buffer identity is again in set_vertex_buffers. + // + // WHY IT TRAVELS AT ALL, since no backend has ever read a binding point (the frontend + // resolves them eagerly into the flat view above, and grep finds zero backend reads of + // VertexBufferBindingPoint / GetAttributeBindingIndex / GetAttributeRelativeOffset): the + // record DECLARES BindingPointCount, PipeFields.def names it, and a record whose declared + // counts do not describe its own blob is a shape the applier's bounds gate would have to + // police forever. Carrying both views keeps the record self-describing, and the cost is + // paid once per configuration change rather than per draw - the blob rides only on + // create_vertex_elements. + struct MGPVertexBindingPointWire { + Uint64 Offset; // 0 + Int32 Stride; // 8 GL 4.6 core table 23.4: the INITIAL value is 16, not 0 + Uint32 Divisor; // 12 + }; + // ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every // member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no // SizeT - except the vertex types, which carry SharedPtr by design and are @@ -560,5 +615,16 @@ namespace MobileGL { static_assert(std::is_trivially_copyable_v && sizeof(SamplerParameters) == 100); static_assert(std::is_trivially_copyable_v && sizeof(MG_State::GLState::VertexAttributeVersion) == 6); + // The two P3a wire views. Unlike the structs they mirror these ARE flat PODs with + // explicit padding, so the trip wire is the same one every MGPipe payload carries: the + // blob they ride in is memcpy'd, and a field silently changing width is a protocol break + // no test would otherwise see. (MGP_ASSERT_POD is MGPipeTypes.h's and that header + // includes this one, so the assertions are spelled out here instead.) + static_assert(std::is_trivially_copyable_v && + sizeof(MGPVertexAttribWire) == 24); + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v && + sizeof(MGPVertexBindingPointWire) == 16); + static_assert(std::is_standard_layout_v); } // namespace MobileGL #endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index f9c3a75b..c8c3c01f 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -379,6 +379,12 @@ namespace MobileGL::MG_Pipe { MGPipeApplierState g_applier{}; + // The installed handle-shaped resource table. Null until a backend registers one, + // which is what makes the client half landable on its own: with nothing here every + // frontend dispatch falls through to the op table this one replaces, and the tree + // behaves exactly as it did. + const MGPipeResourceOps* g_resourceOps = nullptr; + MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) { if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr; MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot]; @@ -392,6 +398,9 @@ namespace MobileGL::MG_Pipe { MGPipeApplierState& MGPipeApplier() { return g_applier; } + void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; } + const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; } + void MGPipeApplierReset() { g_applier.RenderStateCsos.clear(); g_applier.BoundRenderStateCso = kMGPipeNullHandle; @@ -402,6 +411,22 @@ namespace MobileGL::MG_Pipe { g_applier.ResidualDivergences = 0; g_applier.PatchCarrierComparisons = 0; g_applier.PatchCarrierDivergences = 0; + // P3a. A fresh context is a fresh server: the records describe objects the new + // context never made, and the three serials are per-context MGGens that must not + // carry a previous context's count into a twin's "have I synced this?" compare. + // The OP TABLE is deliberately NOT cleared here - it is installed and uninstalled by + // the backend's own bring-up and teardown, not by a state reset. + g_applier.Resources.clear(); + g_applier.VertexElementsCsos.clear(); + g_applier.BoundVertexElements = kMGPipeNullHandle; + g_applier.VertexBuffers = {}; + g_applier.VertexBufferStart = 0; + g_applier.VertexBufferCount = 0; + g_applier.VertexFetchBaseInstance = 0; + g_applier.VertexBuffersSerial = 0; + g_applier.IndexBuffer = MGPIndexBuffer{}; + g_applier.IndexBufferSerial = 0; + g_applier.MapPersistentRoundtrips = 0; } void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) { @@ -654,6 +679,75 @@ namespace MobileGL::MG_Pipe { } } + // ================================================================================ + // P3a: the fourteen new entry points, AT THE CONTRACT COMMIT ONLY. + // + // Every body below is a deliberate no-op. The contract commit's job is the SHAPE - the + // signatures the client, the backend and the gates compile against, the records they + // write into and the op table they dispatch through - and the bodies land in the two + // commits that follow on this branch, before anything emits a single one of these calls. + // + // NOTHING REACHES THEM HERE, and that is checked rather than hoped: the two subsystem + // bits are not in MG_Impl/Pipe/PipeFill.cpp's kMGPipeWiredSubsystems, the dirty bits that + // would gate the emission still map to no subsystem, and the emitters beside them are + // stubs that emit nothing. A no-op that could be reached would be worse than an + // unimplemented one - it would silently drop a mutation - which is exactly why the two + // halves land in one commit apiece rather than one half at a time. + // ================================================================================ + + void MGPipeApplyResourceCreate(const MGPResourceDesc& desc) { (void)desc; } + + void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes) { + (void)desc; + (void)initialBytes; + } + + void MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes) { + (void)record; + (void)bytes; + } + + void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes) { + (void)record; + (void)bytes; + } + + void MGPipeApplyResourceFlushRange(const MGPFlushRange& record, const void* bytes) { + (void)record; + (void)bytes; + } + + void MGPipeApplyResourceReadback(const MGPReadback& record) { (void)record; } + + void MGPipeApplyResourceDestroy(const MGPHandleOnly& handle) { (void)handle; } + + // A DECLINE, which is a real answer rather than a failure: the persistent-map acquisition + // is allowed to say no, the caller already has that branch, and null is what it reads. + void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes) { + (void)handle; + (void)size; + (void)seedBytes; + return nullptr; + } + + void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle) { (void)handle; } + + void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes) { + (void)desc; + (void)blobBytes; + } + + void MGPipeApplyBindVertexElements(const MGPHandleOnly& handle) { (void)handle; } + + void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) { (void)handle; } + + void MGPipeApplySetVertexBuffers(const MGPVertexBuffers& hdr, const MGPVertexBuffer* tail) { + (void)hdr; + (void)tail; + } + + void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) { (void)record; } + void MGPipeDeriveRenderStateFields(PipeInputs& inputs) { // The derivation itself lives in MGPipeApplyAccess above, because that is the one // struct PipeInputs names as a friend - see D5 there for what it recomputes and which diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index d37a84b6..dd03cc2e 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -45,6 +45,94 @@ namespace MobileGL::MG_Pipe { Array PipelineBytes{}; }; + // --------------------------------------------------------------------------------- + // P3a: the handle-shaped resource op table (D-A1) + // --------------------------------------------------------------------------------- + + // The SECOND backend op table, beside BufferBackendOps. Registered by the active backend + // at bring-up and cleared at shutdown, exactly as that one is; a null table means "this + // backend has not taken the resource family over", and the frontend then dispatches the + // old way, which is what lets the client half land on its own and what keeps a backend + // whose buffer path is a later phase untouched. + // + // NO FRONTEND TYPE APPEARS HERE, and that is the whole point of the conversion: every + // hook it replaces took a frontend heap reference and four of them read that object's + // shadow bytes. A resource is an MGPipeHandle plus a payload record plus, where the call + // carries content, a companion `const void*`. + // + // THE COMPANION POINTER IS NOT A NEW IDEA - MGPipeApplyCreateRenderState already carries + // a blob beside its POD for the same reason: in monolith a blob needs no MGPBlobRef and + // the pointer is the client's own shadow base, so the call is zero-copy and behaviour is + // unchanged. How those bytes cross under a real transport is that phase's problem and + // that phase's flag edit; resource_respecify deliberately does NOT carry kHasBlob here, + // because a kHasBlob record must own an MGPBlobRef member and MGPResourceDesc has none. + // + // SubDataResident MAY BE NULL and stays nullable on purpose: one backend deliberately + // does not implement it (kOptional in the catalogue), the frontend checks it exactly as + // it checks the op table it replaces, and giving that backend a real implementation is a + // behaviour change that belongs in its own change, not in this migration. + struct MGPipeResourceOps { + void (*Create)(MGPipeHandle res, const MGPResourceDesc& desc); + void (*Respecify)(MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes); + void (*SubData)(MGPipeHandle res, const MGPSubData& record, const void* bytes); + // kOptional: may be null. `bytes` is the application's staging store and is valid for + // the duration of the call only. + void (*SubDataResident)(MGPipeHandle res, const MGPSubData& record, const void* bytes); + void (*FlushRange)(MGPipeHandle res, const MGPFlushRange& record, const void* bytes); + void (*Readback)(MGPipeHandle res, const MGPReadback& record); + void (*Destroy)(MGPipeHandle res); + void* (*MapPersistent)(MGPipeHandle res, Uint64 size, const void* seedBytes); + void (*UnmapPersistent)(MGPipeHandle res); + }; + + // Install / read the table. A null argument uninstalls, which is what a backend does at + // context teardown and what every build that has not migrated the family sits at. + void MGPipeSetResourceOps(const MGPipeResourceOps* ops); + const MGPipeResourceOps* MGPipeGetResourceOps(); + + // --------------------------------------------------------------------------------- + // P3a: the applier's own records (D-G4) + // --------------------------------------------------------------------------------- + + // One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the + // reserved null handle and is never live. + struct MGPipeResourceRecord { + Uint32 Gen = 0; + Bool Live = false; + // The last create/respecify, verbatim. The backend reads its Width / Usage / + // StorageFlags / HasDefinedContent instead of asking the frontend object. + MGPResourceDesc Desc{}; + // SERVER-OWNED, monotone, and it never crosses the line: an MGGen-class counter, ++ on + // every mutation this applier applies (respecify, sub-data, flush range, resident + // sub-data). It is what replaces the frontend change serial the backend used to + // mirror, and no MGPipe call may require the client to provide or know one. + Uint64 Serial = 0; + // ALWAYS FALSE IN P3a, AND WRITTEN BY NOBODY. It exists so the phase that pushes + // persistent-mapped host writes can set it with zero new record kinds; a verify build + // pins that it is false, so that phase cannot land a silent semantic change under it. + Bool HasLiveHostWrites = false; + }; + + // The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus + // the serial the backend's per-VAO twin compares against instead of a wrapping Uint16 + // configuration version plus an identity patch. + // + // The 32 is GL's MAX_VERTEX_ATTRIBS as MobileGL advertises it (kMGPipeMaxVertexAttribs, + // MGPipeTypes.h), which is also the bound the record's two declared counts are checked + // against before the blob is unpacked. + struct MGPipeVertexElementsRecord { + Uint32 Gen = 0; + Bool Live = false; + Uint32 AttributeCount = 0; + Uint32 BindingPointCount = 0; + Array Attributes{}; + Array BindingPoints{}; + // Server-owned MGGen, ++ on every create_vertex_elements applied to this handle - + // including a RE-create on the same handle, which is how a configuration change + // travels (the handle is minted per frontend VAO and Gen moves only on slot reuse). + Uint64 ContentSerial = 0; + }; + struct MGPipeApplierState { // Indexed by slot; slot 0 is the reserved null handle and is never live // (MGPipeHandles.h kMGPipeFirstAllocatableSlot). @@ -84,6 +172,42 @@ namespace MobileGL::MG_Pipe { Uint32 ResidualDivergences = 0; // cumulative Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only Uint32 PatchCarrierDivergences = 0; // cumulative + + // ---- P3a (D-G4). All per context, like everything above. ---- + + // Indexed by MGPipeHandle::Slot of kind Buffer / VertexElementsCso. + Vector Resources; + Vector VertexElementsCsos; + + // The last bind_vertex_elements. Null is legal and means "no VAO bound". + MGPipeHandle BoundVertexElements = kMGPipeNullHandle; + + // The last set_vertex_buffers, as received: the entries, the window they describe, + // and the fetch base instance they are valid for. + Array VertexBuffers{}; + Uint32 VertexBufferStart = 0; + Uint32 VertexBufferCount = 0; + // The RAW value the client sent (MGPVertexBuffers::BaseInstance) resolved by the + // server's own decision about whether to emulate the fetch shift. The client never + // pre-shifts an offset and never learns the answer: emulation is server-owned. + Uint32 VertexFetchBaseInstance = 0; + // Server-owned MGGen, ++ on every applied set_vertex_buffers. It is what retires the + // backend twin's wrapping-Uint16-plus-identity patches. + Uint64 VertexBuffersSerial = 0; + + // The last set_index_buffer. Independent of the vertex-elements configuration by + // design (D5): the index slot is not part of a VAO's configuration version. + MGPIndexBuffer IndexBuffer{}; + Uint64 IndexBufferSerial = 0; + + // Every map_persistent EMISSION, i.e. every acquisition attempt - mint OR decline - + // because every one of them needs an answer from the resource owner. In monolith the + // answer is free; under a transport it is a real round trip. The number is therefore + // the same in both modes and is "one per storage definition", which is what makes it + // assertable today instead of a counter that can only ever read zero. The counter an + // operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the + // applier-side observable a unit case reads without a stats window. + Uint64 MapPersistentRoundtrips = 0; }; // The monolith's single applier. Under split there is one per served context. @@ -121,6 +245,80 @@ namespace MobileGL::MG_Pipe { // block - which is the point. A disagreement is Fatal{PipeResidualDiverged, ""}. void MGPipeApplySetResidualValueState(const ResidualValueBlock& block); + // --------------------------------------------------------------------------------- + // P3a: the nine resource entry points (D-A1, D-A2) + // --------------------------------------------------------------------------------- + // + // These are the ONE exception to push-at-validate: they are applied at the GL call that + // causes them, from the same dispatchers that call the old op table today, because that + // is already where those hooks run. Nothing about buffers moves to validate time here. + // + // The `bytes` companion of the three content-carrying calls is the client's shadow base, + // never a copy (see MGPipeResourceOps). A null is a real answer wherever the payload says + // the content is undefined. + // + // AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB. The signatures are what the client, + // the backend and the gates compile against, and the records above are what they write + // into; the bodies land in the two commits that follow this one on the same branch. + + // resource_create: mints the record and marks the slot Live. Emitted from the buffer + // object's CONSTRUCTOR, so a resource exists before anything can name it; storage is + // defined lazily by the first respecify and a backend tolerates a resource with none. + void MGPipeApplyResourceCreate(const MGPResourceDesc& desc); + // resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is + // the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call, + // MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks. + void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes); + // resource_subdata, buffer half: the destination range rides in the record's box through + // MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split. + // The applier stores nothing per record - contents are the backend's - and bumps Serial. + void MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes); + // buffer_subdata_resident: same shape; `bytes` is the application's staging store and is + // valid for the duration of the call only. The op-table entry may be null. + void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes); + // resource_flush_range: record.AccessFlags are the application's REAL mapping flags, not + // a normalised subset - the backend reads them per call to choose its upload shape. + void MGPipeApplyResourceFlushRange(const MGPFlushRange& record, const void* bytes); + // resource_readback: whole-buffer by contract. The answer travels back through the + // reverse channel, and the writeback happens BEFORE the mutation epoch bumps, never + // after - the ordering is a correctness rule, not a preference. + void MGPipeApplyResourceReadback(const MGPReadback& record); + // resource_destroy: clears Live and drops the record, then the backend frees its twin. + // The CLIENT frees the slot afterwards, in that order, because the allocator forgets the + // lifetime id on free and a notice resolved twice finds nothing the second time. + void MGPipeApplyResourceDestroy(const MGPHandleOnly& handle); + // map_persistent: bumps MapPersistentRoundtrips and asks the backend. Returns the + // coherent host pointer the resource owner donated, or null for a DECLINE - which is a + // real answer and the reason the call is kOptional as well as kReplySlot. `seedBytes` is + // the shadow, still live at this point, for the backends that seed the new store from it. + void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes); + // unmap_persistent: the donation ends. Never emitted by P3a's own paths; the call exists + // so the pair is complete and the transport has both halves. + void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle); + + // --------------------------------------------------------------------------------- + // P3a: the five vertex-input entry points (D-G, D-H, D-I) + // --------------------------------------------------------------------------------- + + // create_vertex_elements. `blobBytes` is MGPVertexAttribWire[desc.AttributeCount] + // immediately followed by MGPVertexBindingPointWire[desc.BindingPointCount], both in + // ascending index order. The applier REFUSES a record whose declared counts do not + // describe its own blob, and both counts are bounded by kMGPipeMaxVertexAttribs. + // Re-issuing on the same handle is how a configuration change travels; it bumps + // ContentSerial and does not rebind. + void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes); + // bind_vertex_elements. The null handle is legal and means "no VAO bound". + void MGPipeApplyBindVertexElements(const MGPHandleOnly& handle); + // delete_vertex_elements: emitted from ONE place, the frontend object's death notice. + void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle); + // set_vertex_buffers: `tail` is hdr.Count MGPVertexBuffer entries starting at hdr.Start. + // hdr.BaseInstance is the DRAW's raw base instance; the applier resolves whether to shift + // and stores the answer in VertexFetchBaseInstance. Bumps VertexBuffersSerial. + void MGPipeApplySetVertexBuffers(const MGPVertexBuffers& hdr, const MGPVertexBuffer* tail); + // set_index_buffer: an independent call, NOT a subset of the vertex-elements + // configuration. Bumps IndexBufferSerial. + void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record); + // --------------------------------------------------------------------------------- // The derivation step (ARCHITECTURE.md 5.3, P2 brief D5) // --------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 469cf3bf..9c368fb1 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -16,6 +16,12 @@ // kScreen lands in struct MGPipeScreen, every other class in struct // MGPipeContext (plan section 4.3). // Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional +// 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 +// allowed a synchronous ack - and MGPipeResourceRespecifyNeedsAck(desc) +// (MGPipeTypes.h) is what says so, which is why the same call still carries +// every glBufferData without acknowledging one. // // RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their // line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new @@ -73,7 +79,7 @@ /* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \ X(GetCaps, MGPCaps, kScreen, kReplySlot) \ X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \ - X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \ + X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck) \ X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \ X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \ X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \ diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index 61644b1c..6ed0b7e2 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -97,7 +97,7 @@ F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex) #define MGP_FIELDS_MGPVertexBuffers(F) \ - F(Start) F(Count) F(ContentHash) + F(Start) F(Count) F(BaseInstance) F(ContentHash) #define MGP_FIELDS_MGPIndexBuffer(F) \ F(Res) F(Offset) F(IndexSize) @@ -295,6 +295,21 @@ #define MGP_FIELDS_MGHostSpan(F) \ F(Ptr) F(Seg) F(Size) F(Offset) +// P3a's two vertex wire views (MGPipeValueTypes.h). They are not call payloads either: they +// are the ELEMENTS of create_vertex_elements' blob, and the comparator has to see into them +// for the same reason it sees into the value structs - a blob compared with memcmp would +// false-differ on MGPVertexAttribWire::Pad0. Divisor is deliberately not in the attribute +// list (it travels in MGPVertexBuffer) and the two Legacy* query answers are deliberately not +// on the wire at all; both absences are argued in MGPipeValueTypes.h and both are enforced +// here by gen_pipe.py's "every list names exactly its struct's direct members" rule. + +#define MGP_FIELDS_MGPVertexAttribWire(F) \ + F(Offset) F(Stride) F(Type) F(Size) F(Enabled) F(Normalized) F(IsInteger) F(IsLong) F(IsBgra) \ + F(BindingIndex) + +#define MGP_FIELDS_MGPVertexBindingPointWire(F) \ + F(Offset) F(Stride) F(Divisor) + // Every payload above, in the order the comparator is generated. Keep in sync with the // macros; gen_pipe.py reads THIS list to know what to emit. #define MGP_VERIFY_PAYLOAD_LIST(P) \ @@ -312,6 +327,7 @@ P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \ P(MGPSurfaceInfo) \ P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \ - P(DynamicBackendParameters) P(MGHostSpan) + P(DynamicBackendParameters) P(MGHostSpan) \ + P(MGPVertexAttribWire) P(MGPVertexBindingPointWire) // clang-format on diff --git a/MobileGL/MG_Pipe/generated/PipeFilled.inc b/MobileGL/MG_Pipe/generated/PipeFilled.inc index d24e5142..d16bbc57 100644 --- a/MobileGL/MG_Pipe/generated/PipeFilled.inc +++ b/MobileGL/MG_Pipe/generated/PipeFilled.inc @@ -306,6 +306,7 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = enum class MGPipeFieldEmitter : Uint8 { kNone = 0, BindRenderState, + BindVertexElements, CreateRenderState, SetDynamicState, SetPatchState, @@ -315,6 +316,7 @@ enum class MGPipeFieldEmitter : Uint8 { inline constexpr const char* kMGPipeFieldEmitterNames[] = { "kNone", "BindRenderState", + "BindVertexElements", "CreateRenderState", "SetDynamicState", "SetPatchState", @@ -327,7 +329,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount MGPipeFieldEmitter::CreateRenderState, // GetBlendEquationIndexed MGPipeFieldEmitter::CreateRenderState, // GetBlendFuncIndexed MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackName - MGPipeFieldEmitter::kNone, // GetBoundVertexArray + MGPipeFieldEmitter::BindVertexElements, // GetBoundVertexArray MGPipeFieldEmitter::kNone, // GetBufferBindingSlot MGPipeFieldEmitter::kNone, // GetBufferBindingPoint MGPipeFieldEmitter::kNone, // GetBufferBindingPointCount @@ -386,7 +388,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan }; -inline constexpr SizeT kMGPipeEmittedFieldCount = 33; +inline constexpr SizeT kMGPipeEmittedFieldCount = 34; struct MGPipeFilledState { Uint64 CurrentVerbSerial; diff --git a/MobileGL/MG_Pipe/generated/PipeVerify.inc b/MobileGL/MG_Pipe/generated/PipeVerify.inc index c4c4661a..fdc5fbd3 100644 --- a/MobileGL/MG_Pipe/generated/PipeVerify.inc +++ b/MobileGL/MG_Pipe/generated/PipeVerify.inc @@ -113,6 +113,8 @@ inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField); inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField); inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField); +inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField); +inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField); template <> struct MGPipeHasFieldVerifier : std::true_type {}; @@ -252,6 +254,10 @@ 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 inline Bool MGPipeFieldEqual(const T& a, const T& b) { @@ -643,6 +649,16 @@ inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** return true; } +inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField) { + MGP_FIELDS_MGPVertexAttribWire(MGP_VERIFY_FIELD) + return true; +} + +inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField) { + MGP_FIELDS_MGPVertexBindingPointWire(MGP_VERIFY_FIELD) + return true; +} + #undef MGP_VERIFY_FIELD -inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69; +inline constexpr SizeT kMGPipeVerifiedPayloadCount = 71; diff --git a/MobileGL/MG_Test/Pipe/CMakeLists.txt b/MobileGL/MG_Test/Pipe/CMakeLists.txt index 5e3fce1a..5669bd3b 100644 --- a/MobileGL/MG_Test/Pipe/CMakeLists.txt +++ b/MobileGL/MG_Test/Pipe/CMakeLists.txt @@ -135,8 +135,42 @@ if (MSVC) target_compile_options(MagmaPipeIdentityTest PRIVATE /Zc:preprocessor) endif() +# P3a's two suites. Their targets and this registration are the CONTRACT commit's, for the +# same reason the four P2 suites' are: their CONTENTS belong to two later packages each, and +# neither of them should have to come back to this file to add a case. +# +# They link gtest rather than gtest_main and carry their own main(), like PipeInputsTest and +# RenderStateSpansTest: the applier's bounds and protocol trip wires report through a log line +# in a shipped push build and std::abort() in a poison or verify one, so a case that drives +# one reads the line back out of a file the process names before anything logs. Deciding that +# HERE is what keeps the later packages out of this file. +foreach(pipeTest ResourceEmitTest VertexInputEmitTest) + add_executable(${pipeTest} ${pipeTest}.cpp) + + target_include_directories(${pipeTest} PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL + ${MGL_ROOT}/MobileGL/MG_Pipe + ${MGL_ROOT}/3rdparty/xxHash + ${MGL_ROOT}/3rdparty/Vulkan-Headers/include + ${MGL_ROOT}/3rdparty/SPIRV-Reflect + ) + + target_link_libraries(${pipeTest} PRIVATE + GTest::gtest + ${LINK_LIBRARIES} + ) + + if (MSVC) + target_compile_options(${pipeTest} PRIVATE /Zc:preprocessor) + endif() +endforeach() + include(GoogleTest) gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) +foreach(pipeTest ResourceEmitTest VertexInputEmitTest) + gtest_discover_tests(${pipeTest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) +endforeach() gtest_discover_tests(MagmaPipeIdentityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(RenderStateSpansTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 1baf1f3e..417ee2b7 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -349,15 +349,20 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) { } // The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and -// the struct that used to memcmp is compared member by member. +// the struct that used to memcmp is compared member by member. P3a added the two vertex wire +// views as a seventh and eighth non-payload entry (63 + 8), for the same reason: they are the +// elements of create_vertex_elements' blob, and a memcmp over that blob would false-differ on +// MGPVertexAttribWire::Pad0. TEST(PipeCatalogue, SixValueStructsHaveFieldLists) { - EXPECT_EQ(kMGPipeVerifiedPayloadCount, 69u); + EXPECT_EQ(kMGPipeVerifiedPayloadCount, 71u); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); + static_assert(MGPipeHasFieldVerifier::value); + static_assert(MGPipeHasFieldVerifier::value); PixelStoreParameters p{}; PixelStoreParameters q{}; const char* field = nullptr; @@ -468,3 +473,113 @@ TEST(PipeCatalogue, SubDataBufferRangeRidesInTheUnionBox) { EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull); EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull); } + +// P3a, D-H1: set_vertex_buffers carries the vertex-FETCH base instance explicitly, one per +// emitted set rather than one per entry, and the header grew 16 -> 24 bytes to hold it. +// +// The size is the cheap half. The half a compiler cannot catch is the PipeFields.def row: +// MGPVertexBuffers still HAS a ContentHash and still asserts its size whether or not the +// field list names BaseInstance, and a comparator blind to the field would let a +// baseInstance-only divergence through under MOBILEGL_PIPE_VERIFY - which is the one gate +// that would otherwise have seen the suppression bug the ContentHash rule exists to prevent. +// So the field list is pinned the only way it can be: by making the comparator name it. +TEST(PipeCatalogue, VertexBufferSetCarriesAnExplicitBaseInstance) { + static_assert(sizeof(MGPVertexBuffers) == 24); + static_assert(sizeof(MGPVertexBuffer) == 32); // the per-entry struct did NOT change + EXPECT_EQ(sizeof(MGPVertexBuffers), 24u); + + MGPVertexBuffers a{}; + MGPVertexBuffers b{}; + const char* field = nullptr; + EXPECT_TRUE(MGPipeVerify(a, b, &field)); + b.Pad0 = 0x5A; // padding is not a field + EXPECT_TRUE(MGPipeVerify(a, b, &field)); + b.Pad0 = 0; + b.BaseInstance = 7; + EXPECT_FALSE(MGPipeVerify(a, b, &field)); + EXPECT_STREQ(field, "BaseInstance"); + + // The set still carries no fetch shift per entry: an entry that disagreed with its own + // header is a shape the applier would have to police, and MGPVertexBuffer's Pad0 stays + // padding rather than becoming a second copy of the same number. + MGPVertexBuffer left{}; + MGPVertexBuffer right{}; + right.Pad0 = 0x5A; + EXPECT_TRUE(MGPipeVerify(left, right, &field)); +} + +// P3a, D-G2: the two vertex wire views. They are what create_vertex_elements' blob is made +// of, so their sizes are the blob's stride and the applier's bounds arithmetic; and IsLong is +// carried SEPARATELY from Type, because a GL_DOUBLE format converted to float and a long +// format that keeps all 64 bits are different requests that a backend has to tell apart. +TEST(PipeCatalogue, VertexWireViewsAreFlatAndCarryIsLongSeparately) { + static_assert(sizeof(MGPVertexAttribWire) == 24); + static_assert(sizeof(MGPVertexBindingPointWire) == 16); + EXPECT_EQ(sizeof(MGPVertexAttribWire), 24u); + EXPECT_EQ(sizeof(MGPVertexBindingPointWire), 16u); + + MGPVertexAttribWire a{}; + MGPVertexAttribWire b{}; + const char* field = nullptr; + EXPECT_TRUE(MGPipeVerify(a, b, &field)); + b.Pad0 = 0x5A; + EXPECT_TRUE(MGPipeVerify(a, b, &field)); + b.Pad0 = 0; + // Type unchanged, IsLong moved: a comparator that folded the two would miss this. + b.IsLong = 1; + EXPECT_FALSE(MGPipeVerify(a, b, &field)); + EXPECT_STREQ(field, "IsLong"); + + MGPVertexBindingPointWire p{}; + MGPVertexBindingPointWire q{}; + EXPECT_TRUE(MGPipeVerify(p, q, &field)); + q.Divisor = 2; + EXPECT_FALSE(MGPipeVerify(p, q, &field)); + EXPECT_STREQ(field, "Divisor"); +} + +// P3a, D-A5: the tree's FIRST kNeedsAck, and the reason it is not a bare flag. +// +// Flags are a PER-CALL static property and resource_respecify serves both glBufferData and +// glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData in a +// world upload - a round trip per chunk store the moment a transport is under it. So the flag +// declares that records of this call MAY need one and MGPipeResourceRespecifyNeedsAck decides +// per record: only an immutable store, which is a real synchronous allocation. +// +// This is the negative control for a future flag that over-acks: in monolith the ack is a +// no-op, so the mistake cannot be shipped from here, and the phase where it would bite +// inherits this pin rather than the guess. +TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) { + Uint32 flags = 0; +#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags) \ + if (std::strcmp(#Name, "ResourceRespecify") == 0) flags = static_cast(Flags); + MGP_CALL_LIST(MGP_FLAGS_OF_RESOURCE_RESPECIFY) +#undef MGP_FLAGS_OF_RESOURCE_RESPECIFY + EXPECT_EQ(flags & static_cast(kNeedsAck), static_cast(kNeedsAck)); + // 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) \ + if ((static_cast(Flags) & static_cast(kNeedsAck)) != 0) ++ackingCalls; + MGP_CALL_LIST(MGP_COUNT_ACKING_CALLS) +#undef MGP_COUNT_ACKING_CALLS + EXPECT_EQ(ackingCalls, 1u); + + // glBufferStorage: an immutable store, and the one entry point allowed a synchronous ack. + MGPResourceDesc immutable{}; + immutable.Immutable = 1; + EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutable)); + + // glBufferData through the same call: never acknowledged, whatever else the descriptor + // says. The usage hint and a defined initial content are the two things a "well it looks + // synchronous" reading would key on, so both are set here on purpose. + MGPResourceDesc mutableStore{}; + mutableStore.Immutable = 0; + mutableStore.Usage = 0x88E4; // GL_STATIC_DRAW, i.e. the most "final-looking" hint there is + mutableStore.HasDefinedContent = 1; + mutableStore.Width = 64u * 1024u; + EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore)); + + // And the opcode did not move: a flag-word edit is not a catalogue edit. + EXPECT_EQ(static_cast(MGPWireOp::ResourceRespecify), 3); +} diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp new file mode 100644 index 00000000..14c56423 --- /dev/null +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -0,0 +1,117 @@ +// MobileGL - MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P3a's resource family: the applier's record lifecycle and the client's emission of it. +// +// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT. +// Two packages fill this file in and neither of them touches MG_Test/Pipe/CMakeLists.txt to +// do it: the applier-side cases (a create marks the slot live, a respecify replaces the +// descriptor and bumps Serial, a destroy clears Live, a stale generation resolves to nothing, +// HasLiveHostWrites is false on every path this phase has, and the sub-data range encoding at +// both of its bounds with the emitter's splitter over it) belong to the branch that gives the +// entry points their bodies; the emitter-side cases (the sticky BindMask over every buffer +// target, on create AND on a following respecify; the slot released at destruction) belong to +// the client branch. They are disjoint TEST bodies in one file. +// +// THE SUITE IS `ResourceEmit`, not `ResourceEmitTest`: the file is XTest.cpp and the suite is +// X, which is this directory's convention (RenderStateSpansTest.cpp -> RenderStateSpans), and +// it is what the phase's gate greps for (`ctest -R '...|ResourceEmit\.'`). +// +// IT HAS ITS OWN main(), like PipeInputsTest and RenderStateSpansTest, and that is a decision +// taken here so that nobody has to come back to the CMake file for it: the applier's bounds +// gate reports through a trip wire whose verdict is a log line in a shipped push build and +// std::abort() in a poison or verify one, so a case that drives it reads the line back out of +// a file this process points MOBILEGL_LOG_FILE_PATH at before anything logs. +// +// Every case is a visible SKIP in a pull build rather than a vanishing test - the applier is +// compiled only under MOBILEGL_PIPE_PUSH - so `ctest -N` stays name-for-name identical +// between the pull and the push trees. + +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +#include "Includes.h" +#include +#if MOBILEGL_PIPE_PUSH +#include +#endif + +using namespace MobileGL; +using namespace MobileGL::MG_Pipe; + +namespace { + String g_logPath; + + int ProcessId() { +#if defined(_WIN32) + return _getpid(); +#else + return static_cast(getpid()); +#endif + } + + // The op table is INSTALLED BY A BACKEND, at its own bring-up, and uninstalled at its + // teardown - it is not part of the applier's state and MGPipeApplierReset deliberately + // does not clear it. A process with no backend in it therefore has none, and that is the + // fact the whole family's landability rests on: with no table registered every frontend + // dispatch falls through to the op table this one replaces, so the client half can land + // on its own without changing a single observable. + // + // It is also the negative control for the registration itself. A Set that did not stick + // would leave the family permanently dark, and nothing else in the tree would say so. + TEST(ResourceEmit, TheResourceOpTableIsUnregisteredUntilABackendInstallsOne) { +#if !MOBILEGL_PIPE_PUSH + GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build"; +#else + ASSERT_EQ(MGPipeGetResourceOps(), nullptr) + << "something registered a resource op table in a unit-test process"; + + static const MGPipeResourceOps ops{}; + MGPipeSetResourceOps(&ops); + EXPECT_EQ(MGPipeGetResourceOps(), &ops); + + // A state reset is not a teardown: the table survives it, because the backend that + // installed it is still there. + MGPipeApplierReset(); + EXPECT_EQ(MGPipeGetResourceOps(), &ops); + + MGPipeSetResourceOps(nullptr); + EXPECT_EQ(MGPipeGetResourceOps(), nullptr); +#endif + } +} // namespace + +int main(int argc, char** argv) { + // Before anything logs: the logger reads this variable once, on its first write, and + // caches the handle. The name carries this process's pid, and the file is removed on the + // way out. + namespace fs = std::filesystem; + const fs::path path = + fs::temp_directory_path() / ("mobilegl-resourceemit-test-" + std::to_string(ProcessId()) + ".log"); + std::error_code ec; + fs::remove(path, ec); + g_logPath = path.string(); +#if defined(_WIN32) + _putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str()); +#else + setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); +#endif + ::testing::InitGoogleTest(&argc, argv); + const int rc = RUN_ALL_TESTS(); + fs::remove(path, ec); + return rc; +} diff --git a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp new file mode 100644 index 00000000..8307b76f --- /dev/null +++ b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp @@ -0,0 +1,122 @@ +// MobileGL - MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P3a's vertex-input family: create/bind/delete_vertex_elements, set_vertex_buffers and +// set_index_buffer, on both sides of the call. +// +// THIS SUITE IS A NAMED GATE. The phase's G6 is "for every VAO configuration the emitted +// MGPVertexElements blob + MGPVertexBuffers set + MGPIndexBuffer reproduce exactly the values +// the backend's VAO twin reads from the frontend today, field by field, for all 32 attribute +// slots", and it is spelled `ctest -R 'VertexInputEmit\.'`; G7 is its negative control, a +// script that stops the wire conversion copying ONE field and expects this suite to go red +// NAMING that field. So a case here must fail by field name, never by a bare count, or the +// control cannot answer. +// +// THE SUITE IS `VertexInputEmit`, not `VertexInputEmitTest`: the file is XTest.cpp and the +// suite is X, this directory's convention (RenderStateSpansTest.cpp -> RenderStateSpans), and +// it is what both gates grep for. +// +// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT - the +// client package writes the conversion cases, the base-instance suppression pair and the +// create/bind ping-pong pair into this file without touching MG_Test/Pipe/CMakeLists.txt. +// +// IT HAS ITS OWN main() for the same reason ResourceEmitTest does: the applier refuses a +// vertex-elements record whose declared counts do not describe its own blob, and that verdict +// is a log line in a shipped push build and std::abort() in a poison or verify one. +// +// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N` +// stays name-for-name identical between the pull and the push trees. + +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +#include "Includes.h" +#include +#if MOBILEGL_PIPE_PUSH +#include +#endif + +using namespace MobileGL; +using namespace MobileGL::MG_Pipe; + +namespace { + String g_logPath; + + int ProcessId() { +#if defined(_WIN32) + return _getpid(); +#else + return static_cast(getpid()); +#endif + } + + // A FRESH CONTEXT IS A FRESH SERVER, and for this family that is not a nicety: the three + // serials are per-context MGGens and the backend's VAO twin decides "have I already + // synced this?" by comparing its own memo against them. A reset that carried a previous + // context's count over would let a twin believe it had synced a configuration it has + // never seen - the one shape the tracker's complete-state rule exists to forbid. + // + // The bound handle is null rather than "whatever was bound", the window is empty rather + // than 32 stale entries, and the fetch shift is 0 rather than the last draw's. + TEST(VertexInputEmit, AResetApplierCarriesNoVertexInputStateOver) { +#if !MOBILEGL_PIPE_PUSH + GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build"; +#else + MGPipeApplierState& applier = MGPipeApplier(); + applier.BoundVertexElements = MGPipeHandle{7, 3}; + applier.VertexBufferStart = 1; + applier.VertexBufferCount = 5; + applier.VertexFetchBaseInstance = 9; + applier.VertexBuffersSerial = 42; + applier.IndexBufferSerial = 43; + applier.MapPersistentRoundtrips = 44; + + MGPipeApplierReset(); + + EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements)); + EXPECT_EQ(MGPipeApplier().VertexBufferStart, 0u); + EXPECT_EQ(MGPipeApplier().VertexBufferCount, 0u); + EXPECT_EQ(MGPipeApplier().VertexFetchBaseInstance, 0u); + EXPECT_EQ(MGPipeApplier().VertexBuffersSerial, 0u); + EXPECT_EQ(MGPipeApplier().IndexBufferSerial, 0u); + EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 0u); + EXPECT_TRUE(MGPipeApplier().VertexElementsCsos.empty()); + EXPECT_TRUE(MGPipeApplier().Resources.empty()); +#endif + } +} // namespace + +int main(int argc, char** argv) { + // Before anything logs: the logger reads this variable once, on its first write, and + // caches the handle. The name carries this process's pid, and the file is removed on the + // way out. + namespace fs = std::filesystem; + const fs::path path = + fs::temp_directory_path() / ("mobilegl-vertexinputemit-test-" + std::to_string(ProcessId()) + ".log"); + std::error_code ec; + fs::remove(path, ec); + g_logPath = path.string(); +#if defined(_WIN32) + _putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str()); +#else + setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); +#endif + ::testing::InitGoogleTest(&argc, argv); + const int rc = RUN_ALL_TESTS(); + fs::remove(path, ec); + return rc; +} diff --git a/MobileGL/MG_Test/Util/PipeStatsTest.cpp b/MobileGL/MG_Test/Util/PipeStatsTest.cpp index b00fbe5f..01777691 100644 --- a/MobileGL/MG_Test/Util/PipeStatsTest.cpp +++ b/MobileGL/MG_Test/Util/PipeStatsTest.cpp @@ -156,6 +156,10 @@ namespace { // the pull build has no CSO to mint and must stay symbol-identical. EXPECT_NE(line.find("cso[csom="), String::npos) << line; EXPECT_NE(line.find("csob="), String::npos) << line; + // P3a's persistent-map acquisition attempts ride the same bracket. It is the counter + // the storage-regrow gate reads, so its short name is pinned where an operator's + // grep would break. + EXPECT_NE(line.find("mpr="), String::npos) << line; #endif } @@ -276,6 +280,7 @@ namespace { #if MOBILEGL_PIPE_PUSH EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints"); EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds"); + EXPECT_STREQ(PS::NameOf(PS::CallClass::MapPersistentRoundtrips), "map-persistent-roundtrips"); #endif EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state"); EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list"); diff --git a/MobileGL/MG_Util/Metrics/PipeStats.cpp b/MobileGL/MG_Util/Metrics/PipeStats.cpp index 858007aa..d30e0273 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.cpp +++ b/MobileGL/MG_Util/Metrics/PipeStats.cpp @@ -174,7 +174,7 @@ namespace MobileGL::MG_Util::PipeStats { "draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect", "tex-upload-jobs", #if MOBILEGL_PIPE_PUSH - "render-state-cso-mints", "render-state-cso-binds", + "render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips", #endif }; const char* const kGateNames[kGateCount] = { @@ -425,6 +425,10 @@ namespace MobileGL::MG_Util::PipeStats { // line an operator greps. line += "] cso[csom=" + std::to_string(calls[static_cast(CallClass::RenderStateCsoMints)]); line += " csob=" + std::to_string(calls[static_cast(CallClass::RenderStateCsoBinds)]); + // P3a's persistent-map acquisition attempts, on the same bracket and for the same + // reason: it is push-only, and a window with an unexpected mpr= is the one number + // that says an adoption is happening per draw rather than per storage definition. + line += " mpr=" + std::to_string(calls[static_cast(CallClass::MapPersistentRoundtrips)]); #endif line += "] gates["; for (Uint32 i = 0; i < kGateCount; ++i) { diff --git a/MobileGL/MG_Util/Metrics/PipeStats.h b/MobileGL/MG_Util/Metrics/PipeStats.h index 49e32d77..f6219fe2 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.h +++ b/MobileGL/MG_Util/Metrics/PipeStats.h @@ -113,6 +113,17 @@ namespace MobileGL::MG_Util::PipeStats { // the cache's hit rate, and it is the number the CSO content-addressing negative // control moves. RenderStateCsoBinds, + // P3a's, and push-only for the same reason as the two above. + // + // EVERY map_persistent EMISSION, i.e. every acquisition ATTEMPT - a mint or a decline + // - because every one of them needs an answer from the resource owner. Counted that + // way on purpose: "round trips actually taken" is 0 by construction in a monolith and + // could never go red, which is not a counter, it is a decoration. Counted as attempts + // the number is identical in both modes, it is exactly "one per storage definition", + // and a regression that acquires per DRAW instead of per definition shows up on the + // first window. Counted at the client emitter, behind the usual Enabled() predicate; + // no timer anywhere. + MapPersistentRoundtrips, #endif Count };