From de532f55a9b1308fd3fd7f0f0ed14deb51091df8 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 6 Sep 2026 08:06:12 -0400 Subject: [PATCH] [Feat] (State): give the frontend six aggregate generations so a tracker can answer "did any bound texture, buffer, attachment or attribute move" with one Uint64 compare - MGP_NOTE_AGGREGATE(Aggregate) next to MGP_NOTE_MUTATION in MG_Pipe/PipeMutation.h, ((void)0) in the pull build for the same reason and with the same shape. It answers a DIFFERENT question from MGP_NOTE_MUTATION - "did any object of this class move since the tracker last looked", not "did a backend move a frontend value inside its own verb" - which is why it is a second macro rather than an overload. - The counters are members of the owning MG_State container (VertexArrayState, FramebufferState, TextureState x2, BufferState) and are reached through a push-only GLContext facade, because the bump points sit on OBJECTS and an object has no back-pointer to the state that owns it. That is the free-function form P2 brief D4 allows, and it costs a global load on a path that has just written object state. - A SIXTH aggregate, VertexAttribDefault on GLContext, which D4 does not list. Its bit (NEW_VERTEX_ATTRIB_DEFAULTS) is specified there with a ContentHash over all 32 CurrentVertexAttributeValues, and hashing 768 bytes on every draw does not fit inside the T1 ceiling the same brief pins. The hash still decides whether to EMIT (D11's set-hash suppressor); the generation decides whether to hash at all. - 30 bump points: 3 VertexArrayObject config-version sites, 3 FramebufferObject object-version sites (one of them inside MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER, so the statement carries its own line continuation), 5 texture content-version sites, 12 texture params-version sites, SamplerObject::BumpVersion as the sampler choke point, 7 BufferObject change-serial sites and the 3 glVertexAttrib* defaults. - Every counter is deliberately COARSER than the state it guards: over-firing costs one extra push, under-firing renders stale, and under-firing is the direction ARCHITECTURE.md 13.2 names as the dangerous one and the P1 verify comparator cannot see for object-class state. - TrackerTest: each bump point moves ITS aggregate and no other, plus a null-context note. - G1: the pull build is 0 added / 0 removed / 0 renamed and the four resized symbols are the contract commit's own, unchanged by this commit. --- MobileGL/MG_Impl/Pipe/PipeFill.cpp | 33 ++++ MobileGL/MG_Pipe/PipeMutation.h | 45 +++++ .../GLState/BufferState/BufferObject.cpp | 8 + .../GLState/BufferState/BufferState.h | 12 ++ MobileGL/MG_State/GLState/Core.cpp | 4 + MobileGL/MG_State/GLState/Core.h | 40 +++++ .../FramebufferState/FramebufferObject.cpp | 4 + .../FramebufferState/FramebufferState.h | 12 ++ .../GLState/SamplerState/SamplerObject.cpp | 4 + .../GLState/TextureState/TextureObject.cpp | 15 ++ .../GLState/TextureState/TextureObject.h | 2 + .../TextureState/TextureObject2DCube.cpp | 3 + .../GLState/TextureState/TextureState.h | 15 ++ .../VertexArrayState/VertexArrayObject.cpp | 4 + .../VertexArrayState/VertexArrayState.h | 13 ++ MobileGL/MG_Test/Pipe/TrackerTest.cpp | 163 ++++++++++++++++-- 16 files changed, 366 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 1276cb4c..6ffeea0c 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -490,6 +490,39 @@ namespace MobileGL::MG_Pipe { MGPipeFillAccess::CopyField(inputs, *ctx, field); } + // ---- the aggregate generations (P2 brief D4) ---- + // MGP_NOTE_AGGREGATE lands here. The bump points are on OBJECTS, which have no + // back-pointer to the state container that owns them, so the note finds the live + // context - the same shape, and for the same reason, as MGPipeNoteFrontendMutation + // above. No verb has to be in flight and no field is stamped: an aggregate generation + // is not a PipeInputs field, it is what the tracker's shutter compares against. + void MGPipeNoteAggregate(MGPipeAggregate aggregate) { + auto* ctx = LiveContext(); + if (ctx == nullptr) return; + switch (aggregate) { + case MGPipeAggregate::VaoAttribute: + ctx->NoteVaoAttributeChanged(); + break; + case MGPipeAggregate::FramebufferAttachment: + ctx->NoteFramebufferAttachmentChanged(); + break; + case MGPipeAggregate::TextureContent: + ctx->NoteTextureContentChanged(); + break; + case MGPipeAggregate::TextureParams: + ctx->NoteTextureParamsChanged(); + break; + case MGPipeAggregate::BufferChange: + ctx->NoteBufferChanged(); + break; + case MGPipeAggregate::VertexAttribDefault: + ctx->NoteVertexAttribDefaultChanged(); + break; + case MGPipeAggregate::Count: + break; + } + } + void MGPipeSetPoisonOmission(const char* verb, const char* field) { if (verb == nullptr || field == nullptr) { g_omission = PoisonOmission{}; diff --git a/MobileGL/MG_Pipe/PipeMutation.h b/MobileGL/MG_Pipe/PipeMutation.h index fcbdc03e..7fd438a7 100644 --- a/MobileGL/MG_Pipe/PipeMutation.h +++ b/MobileGL/MG_Pipe/PipeMutation.h @@ -33,10 +33,55 @@ namespace MobileGL::MG_Pipe { // A no-op unless a context is live, a verb has been filled, and `field` is in that verb // class's may-read mask; a forwarded (sticky) field has no storage and is never copied. void MGPipeNoteFrontendMutation(MGPipeInputField field); + + // ---- the aggregate generations (P2 brief D4, ARCHITECTURE.md 5.2) ---- + // + // MGP_NOTE_MUTATION answers "a backend moved a frontend value INSIDE its own verb". + // MGP_NOTE_AGGREGATE answers a different question, which is why it is a second macro + // and not an overload: "did ANY object of this class move since the last time the + // tracker looked", collapsed onto one monotonic Uint64 per class so a per-verb dirty + // walk is a handful of compares rather than a scan over 32 attributes, 16 attachments, + // 32 texture units and 84 binding points. + // + // The counters are members of the owning MG_State container, all guarded by + // MOBILEGL_PIPE_PUSH so the pull build's state objects do not change size (G1). The + // bump points sit on OBJECTS, which have no back-pointer to their state, so the macro + // goes through a free function that finds the live GLContext - the same shape, and for + // the same reason, as MGP_NOTE_MUTATION (MG_Impl/Pipe/PipeFill.cpp). It costs a global + // load on a path that has just written object state. + // + // Monotonic and never reset: the tracker widens and compares, it never subtracts. + // Over-firing is free (one extra re-push); under-firing renders stale, which is why + // every counter here is deliberately COARSER than the state it guards. + enum class MGPipeAggregate : Uint32 { + // VertexArrayState: any VAO attribute format / buffer / enable moved. + VaoAttribute = 0, + // FramebufferState: any FBO attachment or default-geometry write, or a bind. + FramebufferAttachment, + // TextureState: any texture object CONTENT moved (an upload, a dirty region). + TextureContent, + // TextureState: any texture object or sampler object PARAMETER moved. + TextureParams, + // BufferState: any buffer object contents moved. + BufferChange, + // GLContext: a glVertexAttrib* default value moved. Not one of D4 five: the bit it + // shutters (NEW_VERTEX_ATTRIB_DEFAULTS) is specified there as a ContentHash over + // all 32 CurrentVertexAttributeValues, and hashing 768 bytes on EVERY draw does not + // fit inside the T1 ceiling. The hash still decides whether to EMIT (D11 set-hash + // suppressor); this decides whether to hash at all. + VertexAttribDefault, + Count, + }; + + // MG_Impl/Pipe/PipeFill.cpp. A no-op unless a context is live. + void MGPipeNoteAggregate(MGPipeAggregate aggregate); } // namespace MobileGL::MG_Pipe #define MGP_NOTE_MUTATION(Field) \ ::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field) +#define MGP_NOTE_AGGREGATE(Aggregate) \ + ::MobileGL::MG_Pipe::MGPipeNoteAggregate(::MobileGL::MG_Pipe::MGPipeAggregate::Aggregate) #else #define MGP_NOTE_MUTATION(Field) ((void)0) +#define MGP_NOTE_AGGREGATE(Aggregate) ((void)0) #endif #endif diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 8bfe428b..7109f242 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace MobileGL::MG_State::GLState { namespace { @@ -43,6 +44,7 @@ namespace MobileGL::MG_State::GLState { void BufferObject::NotifyRespecify() { ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); if (g_bufferBackendOps && g_bufferBackendOps->Respecify) { g_bufferBackendOps->Respecify(*this); } @@ -50,6 +52,7 @@ namespace MobileGL::MG_State::GLState { void BufferObject::NotifySubData(SizeT offset, SizeT size) { ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); if (size == 0) return; m_hasDefinedContent = true; if (g_bufferBackendOps && g_bufferBackendOps->SubData) { @@ -59,6 +62,7 @@ namespace MobileGL::MG_State::GLState { void BufferObject::NotifyFlushMappedRange(Range1D range, Flags appAccess) { ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); if (range.start >= range.end) return; m_hasDefinedContent = true; if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) { @@ -73,6 +77,7 @@ namespace MobileGL::MG_State::GLState { // undefined store to "has content" - that would cost the next orphaning // respecification a full-size upload of bytes the application never wrote. ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); return; } m_hasDefinedContent = true; @@ -80,6 +85,7 @@ namespace MobileGL::MG_State::GLState { // The write already landed in coherent GPU memory; the backend has no separate // copy to sync. Only bump the serial so cached transient slices invalidate. ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); return; } NotifySubData(offset, size); @@ -302,6 +308,7 @@ namespace MobileGL::MG_State::GLState { data.size, m_size); Memcpy(m_resource.Bytes() + atOffset, data.data, data.size); ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); } void BufferObject::MarkGpuWritten() { @@ -366,6 +373,7 @@ namespace MobileGL::MG_State::GLState { g_bufferBackendOps->ResidentSubData(*this, offset, bytes); m_hasDefinedContent = true; ++m_changeSerial; + MGP_NOTE_AGGREGATE(BufferChange); m_gpuWritePending = true; return; } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.h b/MobileGL/MG_State/GLState/BufferState/BufferState.h index 51d3b1d6..82ab6dc6 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.h @@ -64,7 +64,19 @@ namespace MobileGL::MG_State::GLState { Bool ValidateName(Uint index) const; Bool ValidateBufferObject(Uint index) const; +#if MOBILEGL_PIPE_PUSH + // P2 brief D4: "did the contents of ANY buffer object move". One counter for every + // BufferObject ++m_changeSerial site, which is what NEW_VERTEX_BUFFERS / + // NEW_INDEX_BUFFER / NEW_CONST_BUFFERS / NEW_SHADER_BUFFERS / NEW_SO_TARGETS all + // shutter on in P2 - five bits over one aggregate until P3b splits them. + void NoteBufferChanged() { ++m_anyBufferChangeGeneration; } + Uint64 GetAnyBufferChangeGeneration() const { return m_anyBufferChangeGeneration; } +#endif + private: +#if MOBILEGL_PIPE_PUSH + Uint64 m_anyBufferChangeGeneration = 0; +#endif UnorderedMap> m_bufferObjects; IndexGenerator m_indexGenerator; Array, GlobalBufferTargets.size()> m_bindingSlots; diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 952e1da8..c814f921 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -15,6 +15,7 @@ #include #include +#include namespace MobileGL::MG_State { void Init() { @@ -213,6 +214,7 @@ namespace MobileGL::MG_State { current.intValue[component] = static_cast(value[component]); current.uintValue[component] = static_cast(value[component]); } + MGP_NOTE_AGGREGATE(VertexAttribDefault); } void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array& value) { @@ -227,6 +229,7 @@ namespace MobileGL::MG_State { current.floatValue[component] = static_cast(value[component]); current.uintValue[component] = static_cast(value[component]); } + MGP_NOTE_AGGREGATE(VertexAttribDefault); } void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array& value) { @@ -241,6 +244,7 @@ namespace MobileGL::MG_State { current.floatValue[component] = static_cast(value[component]); current.intValue[component] = static_cast(value[component]); } + MGP_NOTE_AGGREGATE(VertexAttribDefault); } const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const { diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 3b7e4802..a72398ac 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -198,6 +198,43 @@ namespace MobileGL { Uint GetBoundProgramPipelineName() const { return m_boundProgramPipeline; } const SharedPtr& GetBoundProgramPipeline() const; +#if MOBILEGL_PIPE_PUSH + // ---- the aggregate generations (P2 brief D4) ---- + // + // The bump points sit on OBJECTS - a VertexArrayObject, a TextureObject, a + // BufferObject - which have no back-pointer to the state container that owns + // them, so MGP_NOTE_AGGREGATE goes through MGPipeNoteAggregate, which finds + // the live context and lands here. This facade is the whole reason the + // objects need no back-pointer, and it is push-only so the pull build's + // GLContext is byte-identical (G1). + void NoteVaoAttributeChanged() { m_vertexArrayState.NoteAttributeChanged(); } + Uint64 GetAnyVaoAttributeGeneration() const { + return m_vertexArrayState.GetAnyAttributeGeneration(); + } + void NoteFramebufferAttachmentChanged() { m_framebufferState.NoteAttachmentChanged(); } + Uint64 GetAnyFramebufferAttachmentGeneration() const { + return m_framebufferState.GetAnyAttachmentGeneration(); + } + void NoteTextureContentChanged() { m_textureState.NoteTextureContentChanged(); } + Uint64 GetAnyTextureContentGeneration() const { + return m_textureState.GetAnyTextureContentGeneration(); + } + void NoteTextureParamsChanged() { m_textureState.NoteTextureParamsChanged(); } + Uint64 GetAnyTextureParamsGeneration() const { + return m_textureState.GetAnyTextureParamsGeneration(); + } + void NoteBufferChanged() { m_bufferState.NoteBufferChanged(); } + Uint64 GetAnyBufferChangeGeneration() const { + return m_bufferState.GetAnyBufferChangeGeneration(); + } + // The sixth aggregate lives here rather than on a state container because + // the values it guards do too (m_currentVertexAttributes). + void NoteVertexAttribDefaultChanged() { ++m_anyVertexAttribDefaultGeneration; } + Uint64 GetAnyVertexAttribDefaultGeneration() const { + return m_anyVertexAttribDefaultGeneration; + } +#endif + // RenderState Uint GetRenderStateParametersVersion() const; // Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion. @@ -515,6 +552,9 @@ namespace MobileGL { Bool m_transformFeedbackPaused = false; GLenum m_transformFeedbackPrimitiveMode = GL_POINTS; SharedPtr m_transformFeedbackProgram; +#if MOBILEGL_PIPE_PUSH + Uint64 m_anyVertexAttribDefaultGeneration = 0; +#endif Uint64 m_transformFeedbackGeneration = 0; // Source of the per-span ids above; never rolls back with an object switch. Uint64 m_transformFeedbackNextGeneration = 0; diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp index 92402f06..1261f604 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp @@ -11,6 +11,7 @@ #include "MG_Util/Types.h" #include +#include namespace MobileGL::MG_State::GLState { // Starts at 1 so a zero-initialized memo slot can never carry a live object's id. @@ -205,6 +206,7 @@ namespace MobileGL::MG_State::GLState { if (m_readBuffer == buf) return; m_readBuffer = buf; ++m_objectVersion; + MGP_NOTE_AGGREGATE(FramebufferAttachment); } Uint FramebufferObject::GetExternalIndex() const { @@ -216,6 +218,7 @@ namespace MobileGL::MG_State::GLState { if (member == value) return; \ member = value; \ ++m_objectVersion; \ + MGP_NOTE_AGGREGATE(FramebufferAttachment); \ } MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultWidth, m_defaultWidth, Int) @@ -228,5 +231,6 @@ namespace MobileGL::MG_State::GLState { void FramebufferObject::BumpAttachmentVersion(FramebufferAttachmentType type) { ++m_attachmentVersions[static_cast(type)]; ++m_objectVersion; + MGP_NOTE_AGGREGATE(FramebufferAttachment); } } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h index 3e822be7..94dd27c4 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h @@ -25,7 +25,19 @@ namespace MobileGL::MG_State::GLState { Bool ValidateName(Uint index) const; Bool ValidateFramebufferObject(Uint index) const; +#if MOBILEGL_PIPE_PUSH + // P2 brief D4: "did the attachment set or the default geometry of ANY framebuffer + // move". It does NOT cover a BIND - a bind writes a BindingSlot, not the object - so + // MGPipeTracker pairs this counter with the bound draw framebuffer identity, which + // is one extra load and keeps the bump points on the object where they belong. + void NoteAttachmentChanged() { ++m_anyAttachmentGeneration; } + Uint64 GetAnyAttachmentGeneration() const { return m_anyAttachmentGeneration; } +#endif + private: +#if MOBILEGL_PIPE_PUSH + Uint64 m_anyAttachmentGeneration = 0; +#endif UnorderedMap> m_framebufferObjects; IndexGenerator m_indexGenerator; Array, static_cast(FramebufferTarget::FramebufferTargetCount)> diff --git a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp index 3f91b4ad..a5c301b9 100644 --- a/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp +++ b/MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp @@ -12,6 +12,7 @@ #include #include +#include namespace MobileGL { namespace MG_State { @@ -47,6 +48,9 @@ namespace MobileGL { // bindings must never miss an invalidation, and over-invalidating on a wrap-mode // write costs one re-resolve. if (pGLContext) pGLContext->BumpSamplingResolutionGeneration(); + // Every sampler parameter is a texture PARAMETER as far as the dirty walk is + // concerned, and BumpVersion is the one choke point every setter reaches. + MGP_NOTE_AGGREGATE(TextureParams); } void SamplerObject::SetWrapS(SamplerWrapMode mode) { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 000f4d93..0c4c0575 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -11,6 +11,7 @@ #include "MG_State/GLState/StateObjectDeathNotice.h" #include "MG_Util/Types.h" #include +#include namespace MobileGL { namespace MG_State { @@ -114,6 +115,7 @@ namespace MobileGL { m_internalFormat = format; BumpShapeVersion(); ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } Uint TextureObjectBase::GetExternalIndex() const { @@ -139,6 +141,7 @@ namespace MobileGL { m_sampler->SetBorderColor(color); ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } const IntVec4& TextureObjectBase::GetBorderColorI() const { @@ -153,6 +156,7 @@ namespace MobileGL { m_sampler->SetBorderColorI(color); ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } const UintVec4& TextureObjectBase::GetBorderColorUI() const { @@ -167,6 +171,7 @@ namespace MobileGL { m_sampler->SetBorderColorUI(color); ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } BorderColorForm TextureObjectBase::GetBorderColorForm() const { @@ -216,6 +221,7 @@ namespace MobileGL { break; } ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } void TextureObjectBase::SetSwizzleParamRGBA(const Vec4& values) { @@ -223,6 +229,7 @@ namespace MobileGL { m_swizzleParams = values; ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } const UintVec2& TextureObjectBase::GetLevelRange() const { @@ -240,6 +247,7 @@ namespace MobileGL { m_levelRange.y() = m_levelRange.x(); } ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); BumpShapeVersion(); } @@ -251,6 +259,7 @@ namespace MobileGL { m_levelRange.y() = maxLevel; ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); BumpShapeVersion(); } @@ -271,6 +280,7 @@ namespace MobileGL { m_levelRange.y() = std::min(std::max(m_levelRange.y(), m_levelRange.x()), m_immutableLevels - 1); } ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } Uint16 TextureObjectBase::GetTextureParamsVersion() const { @@ -298,6 +308,7 @@ namespace MobileGL { void TextureObjectBase::BumpContentVersion() { ++m_contentVersion; + MGP_NOTE_AGGREGATE(TextureContent); } Int TextureObjectBase::GetSamples() const { @@ -307,6 +318,7 @@ namespace MobileGL { void TextureObjectBase::SetSamples(Int samples) { m_samples = samples; ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } Bool TextureObjectBase::HasFixedSampleLocations() const { @@ -316,6 +328,7 @@ namespace MobileGL { void TextureObjectBase::SetFixedSampleLocations(Bool fixedSampleLocations) { m_fixedSampleLocations = fixedSampleLocations; ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } Uint64 TextureObjectBase::GetLifetimeId() const { @@ -367,6 +380,7 @@ namespace MobileGL { Bool dirty) { if (dirty) { ++m_contentVersion; + MGP_NOTE_AGGREGATE(TextureContent); } m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty); } @@ -378,6 +392,7 @@ namespace MobileGL { void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset, IntVec3 size) { ++m_contentVersion; + MGP_NOTE_AGGREGATE(TextureContent); m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset, size); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 1cb33cca..efab72c7 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -13,6 +13,7 @@ #include "../SamplerState/SamplerObject.h" #include #include +#include namespace MobileGL::MG_State::GLState { // Texture objects are always SharedPtr-owned (TextureState creates every instance via @@ -188,6 +189,7 @@ namespace MobileGL::MG_State::GLState { if (m_depthStencilTextureMode == mode) return; m_depthStencilTextureMode = mode; ++m_textureParamsVersion; + MGP_NOTE_AGGREGATE(TextureParams); } protected: diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 765477c5..b326d3a5 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "TextureObject2DCube.h" +#include namespace MobileGL { namespace MG_State { @@ -49,6 +50,7 @@ namespace MobileGL { void TextureObject2DCube::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) { if (dirty) { ++m_contentVersion; + MGP_NOTE_AGGREGATE(TextureContent); } m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty); } @@ -60,6 +62,7 @@ namespace MobileGL { void TextureObject2DCube::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset, IntVec3 size) { ++m_contentVersion; + MGP_NOTE_AGGREGATE(TextureContent); m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset, size); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index ab7af05a..4186b6d3 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -140,7 +140,22 @@ namespace MobileGL::MG_State::GLState { // again (the unit tests do exactly that between cases). Uint64 GetContextId() const { return m_contextId; } +#if MOBILEGL_PIPE_PUSH + // P2 brief D4, the two texture aggregates. CONTENT is an upload or a dirty region; + // PARAMS is a glTexParameter or a glSamplerParameter. They are separate because + // NEW_SAMPLER_VIEWS and NEW_SAMPLERS are separate dirty bits and a Minecraft frame + // moves them at wildly different rates. + void NoteTextureContentChanged() { ++m_anyTextureContentGeneration; } + Uint64 GetAnyTextureContentGeneration() const { return m_anyTextureContentGeneration; } + void NoteTextureParamsChanged() { ++m_anyTextureParamsGeneration; } + Uint64 GetAnyTextureParamsGeneration() const { return m_anyTextureParamsGeneration; } +#endif + private: +#if MOBILEGL_PIPE_PUSH + Uint64 m_anyTextureContentGeneration = 0; + Uint64 m_anyTextureParamsGeneration = 0; +#endif static Uint64 AllocateContextId(); const Uint64 m_contextId; diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp index 02a0e5f8..64aef896 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace MobileGL::MG_State::GLState { // Starts at 1 so a zero-initialized memo slot can never carry a live object's id. @@ -313,18 +314,21 @@ namespace MobileGL::MG_State::GLState { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].FormatVersion; ++m_configVersion; + MGP_NOTE_AGGREGATE(VaoAttribute); } void VertexArrayObject::BumpAttributeBufferVersion(Uint index) { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].BufferVersion; ++m_configVersion; + MGP_NOTE_AGGREGATE(VaoAttribute); } void VertexArrayObject::BumpAttributeSwitchVersion(Uint index) { if (index >= MAX_VERTEX_ATTRIBS) return; ++m_attributeVersions[index].SwitchVersion; ++m_configVersion; + MGP_NOTE_AGGREGATE(VaoAttribute); } const VertexAttributeVersion& VertexArrayObject::GetAttributeVersion(Uint index) const { diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.h b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.h index 418813c4..c3ce3207 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.h +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.h @@ -28,7 +28,20 @@ namespace MobileGL { const SharedPtr& GetBoundVertexArray(); Vector>& GetAllVertexArrays(); +#if MOBILEGL_PIPE_PUSH + // P2 brief D4: "did the attribute configuration of ANY vertex array move". + // Bumped from every VertexArrayObject::BumpAttribute*Version through + // MGP_NOTE_AGGREGATE(VaoAttribute), which is coarser than the per-object + // m_configVersion on purpose - the tracker wants one compare, and an extra + // re-push costs a push while a missed one renders stale. + void NoteAttributeChanged() { ++m_anyVaoAttributeGeneration; } + Uint64 GetAnyAttributeGeneration() const { return m_anyVaoAttributeGeneration; } +#endif + private: +#if MOBILEGL_PIPE_PUSH + Uint64 m_anyVaoAttributeGeneration = 0; +#endif // "Nothing bound" (an out-of-range or never-created name was bound). Distinct from // being bound to a live slot so that a slot filled AFTER such a bind does not // retroactively become the bound VAO. diff --git a/MobileGL/MG_Test/Pipe/TrackerTest.cpp b/MobileGL/MG_Test/Pipe/TrackerTest.cpp index b83d6aba..2bdda578 100644 --- a/MobileGL/MG_Test/Pipe/TrackerTest.cpp +++ b/MobileGL/MG_Test/Pipe/TrackerTest.cpp @@ -6,27 +6,168 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The frontend state tracker: the dirty walk, the five aggregate generations, the per-bit fire counters (P2 brief D4). +// The frontend state tracker: the dirty walk, the aggregate generations, the per-bit fire +// counters (P2 brief D4). Owned by P2 package B (p2/tracker); the file and its CMake +// registration are the contract commit's. // -// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its -// CMakeLists.txt registration, so that the package which owns its CONTENTS -// (P2 package B, p2/tracker) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2 -// packages edit the same file, which is what keeps the integrator's rebases clean. -// -// The placeholder case is not decoration: without it the binary has no test, and -// gtest_discover_tests on a binary with no test is a silently green lane. +// Needs the push sources, so every case is a visible SKIP in a pull build rather than a +// vanishing test - the shape PipeInputsTest.cpp established. #include #include "Includes.h" #include +#if MOBILEGL_PIPE_PUSH +#include +#include +#endif + using namespace MobileGL; using namespace MobileGL::MG_Pipe; namespace { - // The tracker does not exist yet; what is true in every build is that the subsystem - // bitmask it dispatches on is allocated and does not overlap the behaviour bit. - TEST(Tracker, PlaceholderUntilTheOwningPackageFillsThisIn) { + // The subsystem bitmask the tracker dispatches on is allocated in every build. + TEST(Tracker, SubsystemBitsDoNotOverlapTheBehaviourBit) { EXPECT_EQ(kMGPipeSubsystemsMigratedAtP2 & kMGPipeBehaviourNoCsoContentAddressing, 0ull); } + +#if !MOBILEGL_PIPE_PUSH + TEST(Tracker, SkippedInAPullBuild) { + GTEST_SKIP() << "the tracker is compiled only under MOBILEGL_PIPE_PUSH"; + } +#else + using GLContext = MG_State::GLState::GLContext; + + using MG_State::GLState::TextureObjectBase; + using MobileGL::TextureTarget; + + constexpr SizeT kAggregateCount = static_cast(MGPipeAggregate::Count); + + // A live frontend context for the bump points to find, restored on the way out so the + // cases stay independent (PipeInputsTest's idiom). + class TrackerAggregates : public ::testing::Test { + protected: + void SetUp() override { + m_previous = Move(MG_State::pGLContext); + MG_State::pGLContext = MakeUnique(); + } + void TearDown() override { MG_State::pGLContext = Move(m_previous); } + + static GLContext& Ctx() { return *MG_State::pGLContext; } + + struct Snapshot { + Uint64 Values[kAggregateCount]; + Uint64 operator[](MGPipeAggregate a) const { return Values[static_cast(a)]; } + }; + + static Snapshot Snap() { + GLContext& c = Ctx(); + Snapshot s{}; + s.Values[static_cast(MGPipeAggregate::VaoAttribute)] = c.GetAnyVaoAttributeGeneration(); + s.Values[static_cast(MGPipeAggregate::FramebufferAttachment)] = + c.GetAnyFramebufferAttachmentGeneration(); + s.Values[static_cast(MGPipeAggregate::TextureContent)] = c.GetAnyTextureContentGeneration(); + s.Values[static_cast(MGPipeAggregate::TextureParams)] = c.GetAnyTextureParamsGeneration(); + s.Values[static_cast(MGPipeAggregate::BufferChange)] = c.GetAnyBufferChangeGeneration(); + s.Values[static_cast(MGPipeAggregate::VertexAttribDefault)] = + c.GetAnyVertexAttribDefaultGeneration(); + return s; + } + + // The whole contract of an aggregate generation in one assertion: the bump point + // moved ITS counter and moved NO other. The second half is what stops a bump point + // being wired to the wrong aggregate, which would over-fire one dirty bit and + // under-fire another - and under-firing is the direction that renders stale. + static void ExpectOnly(MGPipeAggregate moved, const Snapshot& before, const Snapshot& after) { + for (SizeT i = 0; i < kAggregateCount; ++i) { + const auto which = static_cast(i); + if (which == moved) { + EXPECT_GT(after.Values[i], before.Values[i]) << "aggregate " << i << " did not move"; + } else { + EXPECT_EQ(after.Values[i], before.Values[i]) << "aggregate " << i << " moved and must not"; + } + } + } + + UniquePtr m_previous; + }; + + TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) { + const Snapshot s = Snap(); + for (SizeT i = 0; i < kAggregateCount; ++i) EXPECT_EQ(s.Values[i], 0ull); + } + + TEST_F(TrackerAggregates, AVertexArrayAttributeMovesOnlyTheVaoAggregate) { + const auto& vao = Ctx().CreateVertexArrayObject(1); + ASSERT_TRUE(vao != nullptr); + const Snapshot before = Snap(); + vao->EnableAttribute(3); + ExpectOnly(MGPipeAggregate::VaoAttribute, before, Snap()); + } + + TEST_F(TrackerAggregates, AFramebufferObjectWriteMovesOnlyTheFramebufferAggregate) { + const auto& fbo = Ctx().CreateFramebufferObject(1); + ASSERT_TRUE(fbo != nullptr); + fbo->SetReadBuffer(FramebufferAttachmentType::Color0); + const Snapshot before = Snap(); + fbo->SetReadBuffer(FramebufferAttachmentType::Color1); + ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap()); + } + + TEST_F(TrackerAggregates, AFramebufferDefaultSetterMovesOnlyTheFramebufferAggregate) { + const auto& fbo = Ctx().CreateFramebufferObject(2); + ASSERT_TRUE(fbo != nullptr); + const Snapshot before = Snap(); + // The five MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER bodies are one macro, so the + // bump statement inside it has to carry its own line continuation or the macro + // silently swallows the next line. This case is what says it did not. + fbo->SetDefaultWidth(64); + ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap()); + } + + TEST_F(TrackerAggregates, ATextureContentWriteMovesOnlyTheContentAggregate) { + const auto& tex = Ctx().CreateTextureObject(1, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + const Snapshot before = Snap(); + static_cast(tex.get())->BumpContentVersion(); + ExpectOnly(MGPipeAggregate::TextureContent, before, Snap()); + } + + TEST_F(TrackerAggregates, ATextureParameterMovesOnlyTheParamsAggregate) { + const auto& tex = Ctx().CreateTextureObject(2, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + const Snapshot before = Snap(); + tex->SetMaxLevel(4); + ExpectOnly(MGPipeAggregate::TextureParams, before, Snap()); + } + + TEST_F(TrackerAggregates, ASamplerParameterMovesOnlyTheParamsAggregate) { + const auto& sampler = Ctx().CreateSamplerObject(1); + ASSERT_TRUE(sampler != nullptr); + const Snapshot before = Snap(); + sampler->SetWrapS(MobileGL::SamplerWrapMode::ClampToEdge); + ExpectOnly(MGPipeAggregate::TextureParams, before, Snap()); + } + + TEST_F(TrackerAggregates, ABufferRespecifyMovesOnlyTheBufferAggregate) { + const auto& buffer = Ctx().CreateBufferObject(1); + ASSERT_TRUE(buffer != nullptr); + const Snapshot before = Snap(); + buffer->Respecify(64, nullptr); + ExpectOnly(MGPipeAggregate::BufferChange, before, Snap()); + } + + TEST_F(TrackerAggregates, AVertexAttribDefaultMovesOnlyItsOwnAggregate) { + const Snapshot before = Snap(); + Ctx().SetCurrentVertexAttributeFloat(2, Array{1.0f, 2.0f, 3.0f, 4.0f}); + ExpectOnly(MGPipeAggregate::VertexAttribDefault, before, Snap()); + } + + TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) { + UniquePtr held = Move(MG_State::pGLContext); + MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context + MG_State::pGLContext = Move(held); + SUCCEED(); + } +#endif // MOBILEGL_PIPE_PUSH } // namespace