[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.
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent 3d1a866e82
commit de532f55a9
16 changed files with 366 additions and 11 deletions
@@ -11,6 +11,7 @@
#include <Config.h>
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
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<BufferMappingAccessBit> 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;
}
@@ -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<Uint, SharedPtr<BufferObject>> m_bufferObjects;
IndexGenerator<Uint> m_indexGenerator;
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
+4
View File
@@ -15,6 +15,7 @@
#include <Config.h>
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
namespace MobileGL::MG_State {
void Init() {
@@ -213,6 +214,7 @@ namespace MobileGL::MG_State {
current.intValue[component] = static_cast<Int32>(value[component]);
current.uintValue[component] = static_cast<Uint32>(value[component]);
}
MGP_NOTE_AGGREGATE(VertexAttribDefault);
}
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
@@ -227,6 +229,7 @@ namespace MobileGL::MG_State {
current.floatValue[component] = static_cast<Float>(value[component]);
current.uintValue[component] = static_cast<Uint32>(value[component]);
}
MGP_NOTE_AGGREGATE(VertexAttribDefault);
}
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
@@ -241,6 +244,7 @@ namespace MobileGL::MG_State {
current.floatValue[component] = static_cast<Float>(value[component]);
current.intValue[component] = static_cast<Int32>(value[component]);
}
MGP_NOTE_AGGREGATE(VertexAttribDefault);
}
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
+40
View File
@@ -198,6 +198,43 @@ namespace MobileGL {
Uint GetBoundProgramPipelineName() const { return m_boundProgramPipeline; }
const SharedPtr<ProgramPipelineObject>& 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<ProgramObject> 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;
@@ -11,6 +11,7 @@
#include "MG_Util/Types.h"
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
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<SizeT>(type)];
++m_objectVersion;
MGP_NOTE_AGGREGATE(FramebufferAttachment);
}
} // namespace MobileGL::MG_State::GLState
@@ -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<Uint, SharedPtr<FramebufferObject>> m_framebufferObjects;
IndexGenerator<Uint> m_indexGenerator;
Array<BindingSlot<FramebufferObject>, static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount)>
@@ -12,6 +12,7 @@
#include <MG_State/GLState/StateObjectDeathNotice.h>
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
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) {
@@ -11,6 +11,7 @@
#include "MG_State/GLState/StateObjectDeathNotice.h"
#include "MG_Util/Types.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Pipe/PipeMutation.h>
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<TextureSwizzleParam>& 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);
}
@@ -13,6 +13,7 @@
#include "../SamplerState/SamplerObject.h"
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h>
#include <MG_Pipe/PipeMutation.h>
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:
@@ -7,6 +7,7 @@
// End of Source File Header
#include "TextureObject2DCube.h"
#include <MG_Pipe/PipeMutation.h>
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);
}
@@ -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;
@@ -11,6 +11,7 @@
#include <MG_State/GLState/StateObjectDeathNotice.h>
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
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 {
@@ -28,7 +28,20 @@ namespace MobileGL {
const SharedPtr<VertexArrayObject>& GetBoundVertexArray();
Vector<SharedPtr<VertexArrayObject>>& 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.