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