mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feat] (Pipe): push the bound VAO's format, its vertex buffers with an explicit baseInstance, and its index binding as their own calls
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
@@ -770,6 +771,21 @@ namespace MobileGL::MG_Pipe {
|
||||
ctx->RecordError(code, Move(info));
|
||||
}
|
||||
|
||||
// P3a D-H2.1. The draw's RAW vertex-fetch base instance, set immediately before the fill
|
||||
// at the three *BaseInstance draw entry points. It replaces the ambient process global
|
||||
// the backend used to read, which is a shape that cannot cross a pushed boundary; the
|
||||
// value travels as an explicit field of set_vertex_buffers and the SERVER decides
|
||||
// whether to emulate the fetch shift or let GL_EXT_base_instance do it.
|
||||
//
|
||||
// Indirect draws pass nothing: none of the three sites is in an indirect loop, per-command
|
||||
// base instances are resolved server-side out of the indirect commands, and the client
|
||||
// emits 0 for every indirect path.
|
||||
void MGPipeSetPendingBaseInstance(Uint32 baseInstance) {
|
||||
MGPipeTrackerInstance().SetPendingBaseInstance(baseInstance);
|
||||
}
|
||||
|
||||
Uint32 MGPipePendingBaseInstance() { return MGPipeTrackerInstance().PendingBaseInstance(); }
|
||||
|
||||
void MGPipeLeaveVerb() {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
@@ -778,6 +794,11 @@ namespace MobileGL::MG_Pipe {
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
// The pending base instance belongs to the verb that was about to run, so leaving
|
||||
// one drops it. The validate point clears it too, after the emission consumed it -
|
||||
// the two together are what make a plain draw after a base-instanced one see 0
|
||||
// again, and neither of them relies on the other being called.
|
||||
MGPipeTrackerInstance().ClearPendingBaseInstance();
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +919,8 @@ namespace MobileGL::MG_Pipe {
|
||||
kMGPipeSubsystemPixelPack |
|
||||
kMGPipeSubsystemPatchState |
|
||||
kMGPipeSubsystemVertexAttribDefaults |
|
||||
kMGPipeSubsystemResources;
|
||||
kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemVertexInput;
|
||||
|
||||
// A field an emitted call supplies COMPLETELY, so the residual fill may stop pulling
|
||||
// it. Two rows of Coverage.def's emitted list do not qualify and each has its reason
|
||||
@@ -919,10 +941,30 @@ namespace MobileGL::MG_Pipe {
|
||||
// retiring that pull is blocked on A teaching the applier to switch on
|
||||
// ValueClass. EmitVertexAttribDefaults checks rather than trusts, and repairs the
|
||||
// mirror when the applier's write does not reproduce the value.
|
||||
//
|
||||
// GetBoundVertexArray is P3a's row, and Coverage.def asks for the decision to be
|
||||
// taken HERE, deliberately, rather than inherited from the row's presence. THE
|
||||
// ANSWER IS NO, and it is not a matter of degree: the field's storage is a
|
||||
// SharedPtr<VertexArrayObject> - a frontend heap reference - and the call that
|
||||
// supplies it, bind_vertex_elements, carries an eight-byte {slot, gen} handle
|
||||
// and nothing else. The applier stores that handle in
|
||||
// MGPipeApplierState::BoundVertexElements; it has no way to produce the pointer,
|
||||
// and P3a deliberately does not give it one (a payload never contains a pointer,
|
||||
// and the whole point of the conversion is that the server stops holding
|
||||
// frontend references). Skipping the pull would leave m_boundVertexArray null on
|
||||
// every draw of every push build - which is not a subtle staleness, it is every
|
||||
// backend read of the bound VAO reading nothing.
|
||||
//
|
||||
// So the row is EMITTED-AND-STILL-PULLED, exactly like GetPixelStoreParameters:
|
||||
// the call goes out because the server needs the format, and the field keeps
|
||||
// coming through the residual fill because the mirror is a pointer only the
|
||||
// client can hold. What retires the pull is not a better applier - it is P8,
|
||||
// where the backend stops reading a frontend VAO at all.
|
||||
constexpr Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field) {
|
||||
switch (field) {
|
||||
case MGPipeInputField::GetPixelStoreParameters:
|
||||
case MGPipeInputField::GetCurrentVertexAttribute:
|
||||
case MGPipeInputField::GetBoundVertexArray:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
@@ -1274,33 +1316,27 @@ namespace MobileGL::MG_Pipe {
|
||||
return payloadBytes;
|
||||
}
|
||||
|
||||
// ---- P3a's three vertex-input emitters. STUBS AT THE CONTRACT COMMIT. ----
|
||||
// ---- P3a's three vertex-input emitters (D-G3, D-H3, D-I) ----
|
||||
//
|
||||
// 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.
|
||||
// The shape - three functions in the fixed order elements, then buffers, then index,
|
||||
// after the four P2 emitters - is the contract commit's, so that the commit which
|
||||
// fills the bodies in does not also have to edit the validate point. All three now
|
||||
// have bodies and MGPipeSubsystemForDirty maps their bits onto the vertex-input
|
||||
// subsystem, so `wants()` can be true.
|
||||
//
|
||||
// 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.
|
||||
// Everything they do lives in MG_Impl/Pipe/VertexInputEmit.h; what is here is the
|
||||
// adaptation to the validate point's byte-counting contract.
|
||||
Uint64 EmitVertexElements(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
return MGPipeVertexInputEmitterInstance().EmitVertexElements(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitVertexBuffers(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
MGPipeTracker& tracker = MGPipeTrackerInstance();
|
||||
return MGPipeVertexInputEmitterInstance().EmitVertexBuffers(ctx, tracker.PendingBaseInstance());
|
||||
}
|
||||
|
||||
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
return MGPipeVertexInputEmitterInstance().EmitIndexBuffer(ctx);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -1369,6 +1405,10 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeCsoCacheInstance().Reset();
|
||||
MGPipeApplierReset();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
// P3a: and the vertex-input emitter's latches, for the same reason - they say
|
||||
// "this handle has already published this configuration" about an applier whose
|
||||
// vertex-elements records the reset above has just dropped.
|
||||
MGPipeVertexInputEmitterInstance().Reset();
|
||||
g_residualDue = true;
|
||||
}
|
||||
|
||||
@@ -1399,6 +1439,11 @@ namespace MobileGL::MG_Pipe {
|
||||
if (wants(MGPipeDirty::NewIndexBuffer)) {
|
||||
payloadBytes += EmitIndexBuffer(*ctx);
|
||||
}
|
||||
// CONSUMED, so the next verb starts from zero. The tracker's bit-9 shutter read it
|
||||
// above and EmitVertexBuffers put it on the wire; leaving it set would give the next
|
||||
// draw the previous draw's fetch shift, which is the exact defect the explicit field
|
||||
// exists to remove.
|
||||
tracker.ClearPendingBaseInstance();
|
||||
|
||||
// ---- step 4: the residual fill, for what an emitted call did NOT supply ----
|
||||
const Bool applierDerives = ApplierDerivesRenderStateFields();
|
||||
|
||||
@@ -43,6 +43,32 @@ namespace MobileGL::MG_Pipe {
|
||||
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
||||
void MGPipeLeaveVerb();
|
||||
|
||||
// PipeFill.cpp. P3a D-H2.1: the DRAW's raw vertex-fetch base instance, which
|
||||
// set_vertex_buffers now carries as an explicit field.
|
||||
//
|
||||
// It replaces an ambient process global the backend read at VAO sync time, which is a
|
||||
// shape that cannot cross a pushed boundary. The client sends the raw value and never a
|
||||
// pre-shifted offset: whether to emulate the fetch shift or let GL_EXT_base_instance do
|
||||
// it is the SERVER's decision. It is also an input to set_vertex_buffers' content hash
|
||||
// and to the tracker's bit-9 shutter, so a draw whose only change is its base instance
|
||||
// still reaches the emitter and still goes out.
|
||||
//
|
||||
// Call it immediately before MGP_FILL at a draw entry point that takes a baseinstance.
|
||||
// The validate point consumes and clears it, and MGPipeLeaveVerb clears it too, so a
|
||||
// plain draw that follows a base-instanced one sees 0 again.
|
||||
//
|
||||
// [HANDED ON, not done here] The three GL entry points that owe this call are
|
||||
// MG_Impl/GLImpl/Drawing/GL_Drawing.cpp's DrawElementsInstancedBaseVertexBaseInstance,
|
||||
// DrawElementsInstancedBaseInstance and DrawArraysInstancedBaseInstance. That file is
|
||||
// outside this package's ownership (C.5 assigns it to nobody and C.1 does not list it),
|
||||
// so the setter, its consumption, its shutter and its hash all land here and the three
|
||||
// one-line call sites are recorded for the integrator. Until they exist the emitted
|
||||
// BaseInstance is 0 on every draw, which is what the tree does today.
|
||||
void MGPipeSetPendingBaseInstance(Uint32 baseInstance);
|
||||
// What the next set_vertex_buffers will carry. Exists for the unit gate, which drives
|
||||
// the emitter without a draw entry point to set it.
|
||||
Uint32 MGPipePendingBaseInstance();
|
||||
|
||||
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
|
||||
// the value - of `field` at `verb`, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
|
||||
|
||||
@@ -18,7 +18,15 @@
|
||||
// them answers it against a shape the backend rediscovered. P2 lands the MECHANISM and ONE
|
||||
// real consumer (SetVertexAttribDefaults) so the shape is pinned by a test rather than by a
|
||||
// plan; the other six slots exist, are unit-tested, and are wired by the phase that moves
|
||||
// the set they name.
|
||||
// the set they name. P3a wires the second, SetVertexBuffers.
|
||||
//
|
||||
// A WIRED SLOT PUTS A REQUIREMENT ON ITS HASH, and SetVertexBuffers is where that first
|
||||
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
|
||||
// baseInstance is DRAW state and moves without the buffer set moving, so a hash over the
|
||||
// entries alone would suppress a record whose one changed field is the fetch shift and the
|
||||
// server would keep the previous one. MG_Impl/Pipe/VertexInputEmit.h's
|
||||
// MGPipeVertexBufferSetContentHash mixes Start, Count and BaseInstance in for exactly that
|
||||
// reason, and VertexInputEmit's base-instance pair is the test that says so.
|
||||
//
|
||||
// A hash of 0 is reserved for "never emitted", so the first emission always goes out; a
|
||||
// computed 0 is remapped to 1, which costs one collision in 2^64 an extra emission and
|
||||
@@ -34,7 +42,7 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list).
|
||||
enum class MGPipeSuppressorSlot : Uint32 {
|
||||
SetVertexBuffers = 0, // P3b
|
||||
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
||||
SetSamplerViews, // P3b
|
||||
BindSamplerStates, // P3b
|
||||
SetShaderImages, // P4b
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
// (ARCHITECTURE.md 5.1).
|
||||
//
|
||||
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
|
||||
// computed by comparing a shutter against what the tracker last pushed. P2 EMITS for bits
|
||||
// 0..4 only (the value-class ones); bits 5..17 are computed, latched and counted so the
|
||||
// per-bit fire rate is a measurement rather than a plan, and their fields keep going
|
||||
// through the residual fill until P3a/P3b/P4a/P4b.
|
||||
// computed by comparing a shutter against what the tracker last pushed. P2 emitted for bits
|
||||
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and the
|
||||
// rest are still computed, latched and counted so the per-bit fire rate is a measurement
|
||||
// rather than a plan, with their fields going through the residual fill until P3b/P4a/P4b.
|
||||
//
|
||||
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
|
||||
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
|
||||
@@ -61,14 +61,18 @@ namespace MobileGL::MG_Pipe {
|
||||
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
|
||||
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
||||
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
||||
// ---- value class: computed and counted, emitted from P3a on ----
|
||||
NewVertexElements, // the bound VAO's attribute configuration
|
||||
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a; the other three are
|
||||
// still computed and counted, and are emitted from P3b/P4a on ----
|
||||
NewVertexElements, // the bound VAO's attribute configuration -> create/bind_vertex_elements
|
||||
NewShader, // the current program's link version
|
||||
NewShaderBindings, // image units, block bindings, uniform write set
|
||||
NewGlobalConstants, // the default-uniform-block image
|
||||
// ---- object class: computed and counted, emitted from P3b/P4b on ----
|
||||
NewVertexBuffers,
|
||||
NewIndexBuffer,
|
||||
// ---- object class. THE FIRST TWO ARE P3a's, not P3b/P4b's: the roadmap puts
|
||||
// set_vertex_buffers and set_index_buffer in the same phase as the vertex-elements
|
||||
// trio, and this comment said otherwise until the commit that wired them. The rest
|
||||
// are still computed and counted only. ----
|
||||
NewVertexBuffers, // -> set_vertex_buffers (P3a)
|
||||
NewIndexBuffer, // -> set_index_buffer (P3a)
|
||||
NewFramebuffer,
|
||||
NewSamplerViews,
|
||||
NewSamplers,
|
||||
@@ -86,12 +90,19 @@ namespace MobileGL::MG_Pipe {
|
||||
return Uint32{1} << static_cast<Uint32>(bit);
|
||||
}
|
||||
|
||||
// The five P2 emits for.
|
||||
// The five P2 emits for. Each phase's constant survives as the next phase's A/B control
|
||||
// and as what a test compares the subsystem map against, so none of them is edited in
|
||||
// place when a later phase takes more bits over.
|
||||
inline constexpr Uint32 kMGPipeDirtyEmittedAtP2 =
|
||||
MGPipeDirtyBit(MGPipeDirty::NewRenderState) | MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewPixelPack) | MGPipeDirtyBit(MGPipeDirty::NewPatchState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults);
|
||||
|
||||
// The three P3a adds: the vertex-input family, all on one subsystem.
|
||||
inline constexpr Uint32 kMGPipeDirtyEmittedAtP3a =
|
||||
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers) | MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer);
|
||||
|
||||
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
|
||||
"NEW_RENDER_STATE",
|
||||
"NEW_PIPELINE_STATE",
|
||||
@@ -127,8 +138,16 @@ namespace MobileGL::MG_Pipe {
|
||||
return kMGPipeSubsystemPatchState;
|
||||
case MGPipeDirty::NewVertexAttribDefaults:
|
||||
return kMGPipeSubsystemVertexAttribDefaults;
|
||||
// P3a's three, all one subsystem: create/bind_vertex_elements, set_vertex_buffers
|
||||
// and set_index_buffer are the vertex-input family and an operator switching it off
|
||||
// has to get the whole family's legacy arm, not two thirds of it.
|
||||
// PipeFill.cpp's SubsystemForEmitter carries the pairing static_asserts.
|
||||
case MGPipeDirty::NewVertexElements:
|
||||
case MGPipeDirty::NewVertexBuffers:
|
||||
case MGPipeDirty::NewIndexBuffer:
|
||||
return kMGPipeSubsystemVertexInput;
|
||||
default:
|
||||
// Bits 5..17 have no call of their own until P3a/P3b/P4a/P4b, so there is no
|
||||
// The remaining bits have no call of their own until P3b/P4a/P4b, so there is no
|
||||
// subsystem to switch and the residual fill keeps supplying their fields.
|
||||
return 0;
|
||||
}
|
||||
@@ -136,10 +155,18 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// A COMPOSITE shutter, for the bits whose "did anything move" is more than one counter.
|
||||
// It is a hash, so two different states can in principle collide and cost a MISSED fire.
|
||||
// That is acceptable for bits 5..17 and only for them: nothing consumes those bits in
|
||||
// P2, and P3 replaces each with its own exact shutter as it takes the subsystem over.
|
||||
// The five bits P2 EMITS for are never composed - they are widened counters and byte
|
||||
// The five bits P2 emits for are never composed - they are widened counters and byte
|
||||
// compares, neither of which can collide.
|
||||
//
|
||||
// P3a's three ARE composed, so the risk is now real rather than academic, and it is
|
||||
// accepted with its size stated: each mix takes a 64-bit input into a 64-bit
|
||||
// accumulator, so two DIFFERENT vertex configurations collide with probability ~2^-64
|
||||
// per pair, and the inputs are a monotone lifetime id, a monotone configuration version
|
||||
// and a widened slot version - none of which an application can steer. The alternative,
|
||||
// comparing the whole 32-attribute configuration byte for byte on every verb, is the
|
||||
// per-draw cost the shutter exists to avoid. The narrowing that removes the composition
|
||||
// for bit 10 - its own slot version plus the bound object's identity - is what this
|
||||
// phase already did to the one shutter that was composed over an unrelated aggregate.
|
||||
inline constexpr Uint64 MGPipeMixShutter(Uint64 accumulator, Uint64 value) {
|
||||
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
|
||||
return accumulator;
|
||||
@@ -240,12 +267,39 @@ namespace MobileGL::MG_Pipe {
|
||||
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
|
||||
const Uint64 buffers = ctx.GetAnyBufferChangeGeneration();
|
||||
|
||||
now[Index(MGPipeDirty::NewVertexBuffers)] =
|
||||
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity);
|
||||
// The index buffer lives in the bound VAO's element slot and P2 has no cheap
|
||||
// shutter for that slot alone, so it shares the buffer aggregate and over-fires
|
||||
// on any buffer write anywhere. P3b narrows it when it takes the subsystem over.
|
||||
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(buffers, vaoIdentity);
|
||||
// Bit 9. The VAO attribute aggregate mixed with the bound VAO's identity is
|
||||
// already exact for the SET - it is bumped by all three Bump*Version functions,
|
||||
// which are the only writers of an attribute's format, buffer or enable state -
|
||||
// and a driver-id re-mint that moves no client counter is caught server-side by
|
||||
// the backend's own id generation.
|
||||
//
|
||||
// THE PENDING BASE INSTANCE IS MIXED IN, and this is a deviation from the design
|
||||
// note that said "keep the shutter" (recorded in client-v1.md): the draw's
|
||||
// baseInstance is now an EXPLICIT field of set_vertex_buffers and a
|
||||
// ContentHash input, and it moves neither the attribute aggregate nor the VAO
|
||||
// identity. Without it here, a draw whose only change is its base instance would
|
||||
// never reach the emitter at all and the server would keep the previous fetch
|
||||
// shift - which is the same silently-wrong-geometry the backend's
|
||||
// baseInstanceDirty flag exists to prevent, one level further out. It fires
|
||||
// extra only on the draws that actually carry one.
|
||||
now[Index(MGPipeDirty::NewVertexBuffers)] = MGPipeMixShutter(
|
||||
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity), m_pendingBaseInstance);
|
||||
// Bit 10, NARROWED (P3a, D-I). It used to mix the whole buffer-CONTENT aggregate
|
||||
// with the VAO identity and therefore fired on any buffer write anywhere; what
|
||||
// it guards is one binding slot, so it now reads that slot's own version and the
|
||||
// identity of what is bound to it. The version is a WRAPPING Uint16 bumped only
|
||||
// on a real change, so it goes through the widened counter at this boundary; the
|
||||
// bound object's lifetime id joins it because identity is what closes the wrap
|
||||
// hole. The VAO identity stays in the mix because the element slot BELONGS to
|
||||
// the bound VAO - switching VAOs switches slots.
|
||||
Uint64 indexShutter = 0;
|
||||
if (vao) {
|
||||
const auto& indexSlot = vao->GetIndexBufferBindingSlot();
|
||||
const auto& indexObject = indexSlot.GetBoundObject();
|
||||
indexShutter = MGPipeMixShutter(m_indexSlotVersion.Observe(indexSlot.GetVersion()),
|
||||
indexObject ? indexObject->GetLifetimeId() : 0);
|
||||
}
|
||||
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(vaoIdentity, indexShutter);
|
||||
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
@@ -315,6 +369,8 @@ namespace MobileGL::MG_Pipe {
|
||||
m_renderStateVersion.Reset();
|
||||
m_pipelineStateVersion.Reset();
|
||||
m_framebufferBind.Reset();
|
||||
m_indexSlotVersion.Reset();
|
||||
m_pendingBaseInstance = 0;
|
||||
m_pack = PixelStoreParameters{};
|
||||
m_patch = PatchTrio{};
|
||||
m_staged = RenderStateParameters{};
|
||||
@@ -368,6 +424,19 @@ namespace MobileGL::MG_Pipe {
|
||||
AttribDefaults& StagedAttribDefaults() { return m_stagedAttribs; }
|
||||
const AttribDefaults& StagedAttribDefaults() const { return m_stagedAttribs; }
|
||||
|
||||
// ---- P3a D-H2: the draw's vertex-FETCH base instance ----
|
||||
//
|
||||
// It lives HERE rather than in a file static because bit 9's shutter has to see it:
|
||||
// an ambient process global cannot cross a pushed boundary, and the value is now an
|
||||
// explicit field of set_vertex_buffers and an input to its content hash, so a draw
|
||||
// whose only change is its base instance has to reach the emitter. Set immediately
|
||||
// before the fill at the three *BaseInstance draw entry points; CONSUMED and cleared
|
||||
// by the validate point once it has been emitted, so a plain draw that follows one
|
||||
// sees 0 again whether or not anything called MGPipeLeaveVerb in between.
|
||||
void SetPendingBaseInstance(Uint32 baseInstance) { m_pendingBaseInstance = baseInstance; }
|
||||
Uint32 PendingBaseInstance() const { return m_pendingBaseInstance; }
|
||||
void ClearPendingBaseInstance() { m_pendingBaseInstance = 0; }
|
||||
|
||||
private:
|
||||
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
|
||||
|
||||
@@ -383,6 +452,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
|
||||
// that wrapped would let a composite shutter repeat and cost a missed fire.
|
||||
MGPipeWidenedCounter m_framebufferBind;
|
||||
// The BOUND VAO's element-array slot version, widened for the same reason. One
|
||||
// counter over a slot that changes with the bound VAO: a stale high word can only
|
||||
// ADD a fire, never drop one, and the VAO identity in the same mix is what makes a
|
||||
// switch between two VAOs differ whatever their slot versions read.
|
||||
MGPipeWidenedCounter m_indexSlotVersion;
|
||||
Uint32 m_pendingBaseInstance = 0;
|
||||
// Bits 2 and 3 are BitwiseEqual shutters, not counters.
|
||||
PixelStoreParameters m_pack{};
|
||||
PatchTrio m_patch{};
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/VertexInputEmit.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P3a's vertex-input family (brief D-G, D-H, D-I): the bound VAO's
|
||||
// format as create/bind_vertex_elements, its buffers as set_vertex_buffers with an explicit
|
||||
// baseInstance, and its element binding as set_index_buffer.
|
||||
//
|
||||
// UNLIKE THE RESOURCE FAMILY, these three emit at the VALIDATE POINT, from
|
||||
// MGPipeValidateForVerb's step 3 in the fixed order elements -> buffers -> index. That is
|
||||
// the ordinary rule (ARCHITECTURE.md 5.1); the resource family is the one exception to it.
|
||||
//
|
||||
// THE CSO IS IDENTITY-ADDRESSED, NOT CONTENT-ADDRESSED (D-G1, a recorded deviation from
|
||||
// ARCHITECTURE.md's 1024-entry content-addressed scheme). One handle per frontend
|
||||
// VertexArrayObject, minted off its lifetime id, and create_vertex_elements is RE-ISSUED on
|
||||
// the same handle whenever the configuration moves - legal, because MGPipeHandle::Gen
|
||||
// increments only on slot reuse and never on a respecify. Espryt has no vertex-elements CSO
|
||||
// to share: its twin owns one driver VAO name plus 64 scratch buffer ids, which two frontend
|
||||
// VAOs cannot share, so content addressing would be strictly slower on the only backend this
|
||||
// phase touches. P7 adds the hash-probe-memcmp layer above these same three calls when
|
||||
// Magma's VertexInputStateFactory takes the CSO over.
|
||||
//
|
||||
// WHAT THE UNIT GATE READS. G6 is "the emitted blob + set + index record reproduce exactly
|
||||
// what the backend's VAO twin reads from the frontend today, field by field, for all 32
|
||||
// slots", and G7 is a scripted control that stops the conversion copying ONE field and
|
||||
// expects the suite to go red NAMING it. So the conversion is a pure function per field
|
||||
// (MGPipeBuildVertexAttribWire / MGPipeBuildVertexBindingPointWire) and the staging buffers
|
||||
// the emitter builds into are readable afterwards - the emitter passes m_blob and m_entries
|
||||
// straight to the applier, so "what was emitted" costs no copy at all.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h states in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-G2: the wire conversion, one pure function per view
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// EVERY FIELD OF VertexAttribute THE WIRE FORM CARRIES, and nothing else:
|
||||
//
|
||||
// Divisor is deliberately absent - it is resolved per binding point and travels in
|
||||
// MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads
|
||||
// it. Carrying it twice would let a malformed record disagree with itself.
|
||||
// LegacyStride / LegacyPointer are deliberately absent - they are the
|
||||
// glGetVertexAttrib* query answers and nothing but the query path reads them, so
|
||||
// they stay client-side.
|
||||
// Buffer is deliberately absent - identity travels in set_vertex_buffers, which is
|
||||
// what keeps this record stable while the buffers under it change.
|
||||
// Stride is the RESOLVED distance and a surviving 0 is MEANINGFUL: a pointer call's 0
|
||||
// was already resolved to the element size by the frontend, so a 0 here can only
|
||||
// have come from the binding model, where it means every vertex reads the SAME
|
||||
// element. Collapsing it back into the element size is what made
|
||||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
||||
// IsLong travels SEPARATELY from Type == Float64: VertexAttribFormat(GL_DOUBLE) reads
|
||||
// doubles and asks for them converted to float, VertexAttribLFormat keeps all 64
|
||||
// bits, and the backend's fp64 narrowing and its Adreno disabled-attribute
|
||||
// workaround both key on telling the two apart.
|
||||
inline MGPVertexAttribWire MGPipeBuildVertexAttribWire(const MG_State::GLState::VertexAttribute& attrib,
|
||||
Uint32 bindingIndex) {
|
||||
MGPVertexAttribWire wire{};
|
||||
wire.Offset = static_cast<Uint64>(attrib.Offset);
|
||||
wire.Stride = static_cast<Int32>(attrib.Stride);
|
||||
wire.Type = static_cast<Uint32>(attrib.Type);
|
||||
wire.Size = static_cast<Uint8>(attrib.Size);
|
||||
wire.Enabled = attrib.Enabled ? 1 : 0;
|
||||
wire.Normalized = attrib.Normalized ? 1 : 0;
|
||||
wire.IsInteger = attrib.IsInteger ? 1 : 0;
|
||||
wire.IsLong = attrib.IsLong ? 1 : 0;
|
||||
wire.IsBgra = attrib.IsBgra ? 1 : 0;
|
||||
wire.BindingIndex = static_cast<Uint8>(bindingIndex);
|
||||
return wire;
|
||||
}
|
||||
|
||||
// The ARB_vertex_attrib_binding view. Its initial Stride is 16, not 0 (GL 4.6 core table
|
||||
// 23.4), which is why the wire form keeps it signed and copies it verbatim.
|
||||
inline MGPVertexBindingPointWire
|
||||
MGPipeBuildVertexBindingPointWire(const MG_State::GLState::VertexBufferBindingPoint& point) {
|
||||
MGPVertexBindingPointWire wire{};
|
||||
wire.Offset = static_cast<Uint64>(point.Offset);
|
||||
wire.Stride = static_cast<Int32>(point.Stride);
|
||||
wire.Divisor = static_cast<Uint32>(point.Divisor);
|
||||
return wire;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-H2.3: the content hash, WITH BaseInstance in it
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// A HARD REQUIREMENT, not a nicety. set_vertex_buffers is suppressed on an unchanged
|
||||
// hash (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
|
||||
// fetch shift - exactly the bug the backend's baseInstanceDirty flag exists to prevent.
|
||||
inline Uint64 MGPipeVertexBufferSetContentHash(const MGPVertexBuffer* entries, Uint32 start, Uint32 count,
|
||||
Uint32 baseInstance) {
|
||||
Uint64 hash = XXH64(entries, static_cast<SizeT>(count) * sizeof(MGPVertexBuffer), 0);
|
||||
hash = MGPipeMixShutter(hash, start);
|
||||
hash = MGPipeMixShutter(hash, count);
|
||||
hash = MGPipeMixShutter(hash, baseInstance);
|
||||
return hash;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The emitter
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
class MGPipeVertexInputEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
|
||||
static constexpr SizeT kAttribs = static_cast<SizeT>(VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
static constexpr SizeT kBindings = static_cast<SizeT>(VertexArrayObject::MAX_VERTEX_ATTRIB_BINDINGS);
|
||||
static_assert(kAttribs <= kMGPipeMaxVertexAttribs && kBindings <= kMGPipeMaxVertexAttribs,
|
||||
"both declared counts are bounded by kMGPipeMaxVertexAttribs");
|
||||
|
||||
// create/bind_vertex_elements. D-G3's three arms, verbatim:
|
||||
//
|
||||
// no VAO bound -> bind the null handle (legal, and it means
|
||||
// exactly "no VAO bound")
|
||||
// the bound VAO CHANGED -> (re)create if its configuration moved since
|
||||
// this handle last published one, then bind
|
||||
// the same VAO, configuration MOVED-> create on the SAME handle, and do NOT rebind
|
||||
//
|
||||
// The latch is PER HANDLE, in a slot-indexed table, so ping-ponging between two VAOs
|
||||
// re-binds but never re-creates either. A Uint32 configuration version does not wrap
|
||||
// in any realistic run and is compared directly; the tracker's widened counter is
|
||||
// for the Uint16s and is not needed here.
|
||||
Uint64 EmitVertexElements(GLContext& ctx) {
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
if (!MGPipeHandleIsNull(m_boundHandle)) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||
++m_binds;
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = vao->GetLifetimeId();
|
||||
const Uint32 configVersion = vao->GetConfigVersion();
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId);
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_latch.size()) m_latch.resize(slot + 1);
|
||||
Latch& latch = m_latch[slot];
|
||||
|
||||
Uint64 bytes = 0;
|
||||
const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion ||
|
||||
latch.Gen != handle.Gen;
|
||||
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion);
|
||||
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(handle));
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
m_boundHandle = handle;
|
||||
m_boundLifetimeId = lifetimeId;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// set_vertex_buffers. Espryt consumes RESOLVED attributes, so the set is one entry
|
||||
// per attribute slot with BindingIndex == the attribute index; Start is 0 and Count
|
||||
// is the highest ENABLED attribute plus one, which is the 32-slot prefix walk the
|
||||
// dirty bit is specified over.
|
||||
//
|
||||
// A client-memory array is Res == kMGPipeNullHandle, and that is not a hole: it is
|
||||
// exactly how the server learns "this attribute is client-sourced, upload it
|
||||
// yourself". Its store genuinely does not exist at this moment - the client-array
|
||||
// uploader runs after PrepareForDraw, at the draw entry point - and moving that
|
||||
// resolution to the client is P8's.
|
||||
Uint64 EmitVertexBuffers(GLContext& ctx, Uint32 baseInstance) {
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
Uint32 count = 0;
|
||||
if (vao) {
|
||||
for (SizeT i = 0; i < kAttribs; ++i) {
|
||||
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
|
||||
}
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
const auto& attrib = vao->GetAttribute(static_cast<Uint>(i));
|
||||
MGPVertexBuffer& entry = m_entries[i];
|
||||
entry = MGPVertexBuffer{};
|
||||
entry.Res = attrib.Buffer ? MGPipeSlots().Acquire(MGPipeKind::Buffer,
|
||||
attrib.Buffer->GetLifetimeId())
|
||||
: kMGPipeNullHandle;
|
||||
// The attribute's own byte offset lives in MGPVertexAttribWire::Offset,
|
||||
// so the entry's is the BINDING's, which the frontend already folded in.
|
||||
entry.Offset = 0;
|
||||
entry.Stride = static_cast<Uint32>(attrib.Stride);
|
||||
entry.Divisor = static_cast<Uint32>(attrib.Divisor);
|
||||
entry.BindingIndex = static_cast<Uint32>(i);
|
||||
}
|
||||
}
|
||||
|
||||
const Uint64 hash = MGPipeVertexBufferSetContentHash(m_entries.data(), 0, count, baseInstance);
|
||||
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetVertexBuffers, hash)) {
|
||||
return 0;
|
||||
}
|
||||
m_lastBuffers = MGPVertexBuffers{};
|
||||
m_lastBuffers.Start = 0;
|
||||
m_lastBuffers.Count = count;
|
||||
// THE DRAW'S RAW value. The client never pre-shifts an offset and never learns
|
||||
// whether the server emulated the shift or let GL_EXT_base_instance do it -
|
||||
// emulation is server-owned.
|
||||
m_lastBuffers.BaseInstance = baseInstance;
|
||||
m_lastBuffers.ContentHash = hash;
|
||||
MGPipeApplySetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
++m_bufferSets;
|
||||
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
|
||||
}
|
||||
|
||||
// set_index_buffer. An INDEPENDENT call, not a subset of the vertex-elements
|
||||
// configuration version (D5) - the index slot is explicitly outside the VAO's
|
||||
// m_configVersion, and the shutter for it is bit 10's, narrowed in Tracker.h.
|
||||
//
|
||||
// Offset and IndexSize are 0 here and the draw verb overrides them: at the validate
|
||||
// point there is no draw to read them from, and the applier stores what it is given.
|
||||
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
m_lastIndex = MGPIndexBuffer{};
|
||||
if (vao) {
|
||||
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
|
||||
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
|
||||
}
|
||||
}
|
||||
MGPipeApplySetIndexBuffer(m_lastIndex);
|
||||
++m_indexSets;
|
||||
return sizeof(MGPIndexBuffer);
|
||||
}
|
||||
|
||||
// ---- what a unit case reads. None of it costs a copy: the emitter builds INTO
|
||||
// these and hands the applier the same pointers. ----
|
||||
const Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs>& LastAttributes() const { return m_attributes; }
|
||||
const Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs>& LastBindingPoints() const {
|
||||
return m_bindingPoints;
|
||||
}
|
||||
const MGPVertexElements& LastElements() const { return m_lastElements; }
|
||||
const MGPVertexBuffers& LastVertexBuffers() const { return m_lastBuffers; }
|
||||
const Array<MGPVertexBuffer, kMGPipeMaxVertexAttribs>& LastEntries() const { return m_entries; }
|
||||
const MGPIndexBuffer& LastIndexBuffer() const { return m_lastIndex; }
|
||||
MGPipeHandle BoundHandle() const { return m_boundHandle; }
|
||||
Uint64 CreateCount() const { return m_creates; }
|
||||
Uint64 BindCount() const { return m_binds; }
|
||||
Uint64 VertexBufferSetCount() const { return m_bufferSets; }
|
||||
Uint64 IndexBufferSetCount() const { return m_indexSets; }
|
||||
|
||||
// A fresh context is a fresh server: the applier's records are gone, so every latch
|
||||
// this emitter holds describes objects the server no longer has. Called from the
|
||||
// validate point's FreshlyPrimed arm beside MGPipeApplierReset and the suppressor's
|
||||
// InvalidateAll, for the same reason they are.
|
||||
void Reset() {
|
||||
m_latch.clear();
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
}
|
||||
|
||||
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
|
||||
|
||||
private:
|
||||
struct Latch {
|
||||
Bool Published = false;
|
||||
Uint32 Gen = 0;
|
||||
Uint32 ConfigVersion = 0;
|
||||
};
|
||||
|
||||
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
|
||||
return only;
|
||||
}
|
||||
|
||||
Uint64 EmitCreate(const VertexArrayObject& vao, MGPipeHandle handle, Latch& latch, Uint32 configVersion) {
|
||||
// ALL 32 OF EACH, deliberately. The record DECLARES both counts and the applier
|
||||
// refuses one whose counts do not describe its own blob, so a self-describing
|
||||
// record is the cheap shape - and G6 is stated over all 32 slots, which a
|
||||
// truncated set could not answer. It rides create_vertex_elements only, i.e.
|
||||
// once per configuration change, never per draw.
|
||||
for (SizeT i = 0; i < kAttribs; ++i) {
|
||||
m_attributes[i] = MGPipeBuildVertexAttribWire(vao.GetAttribute(static_cast<Uint>(i)),
|
||||
vao.GetAttributeBindingIndex(static_cast<Uint>(i)));
|
||||
}
|
||||
for (SizeT i = 0; i < kBindings; ++i) {
|
||||
m_bindingPoints[i] = MGPipeBuildVertexBindingPointWire(vao.GetBindingPoint(static_cast<Uint>(i)));
|
||||
}
|
||||
// Attributes first, then binding points, both ascending and contiguous.
|
||||
constexpr SizeT kAttribBytes = kAttribs * sizeof(MGPVertexAttribWire);
|
||||
constexpr SizeT kBindingBytes = kBindings * sizeof(MGPVertexBindingPointWire);
|
||||
std::memcpy(m_blob.data(), m_attributes.data(), kAttribBytes);
|
||||
std::memcpy(m_blob.data() + kAttribBytes, m_bindingPoints.data(), kBindingBytes);
|
||||
|
||||
m_lastElements = MGPVertexElements{};
|
||||
m_lastElements.Cso = handle;
|
||||
m_lastElements.AttributeCount = static_cast<Uint32>(kAttribs);
|
||||
m_lastElements.BindingPointCount = static_cast<Uint32>(kBindings);
|
||||
m_lastElements.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastElements.Blob.Offset = 0;
|
||||
m_lastElements.Blob.Size = kAttribBytes + kBindingBytes;
|
||||
MGPipeApplyCreateVertexElements(m_lastElements, m_blob.data());
|
||||
++m_creates;
|
||||
latch.Published = true;
|
||||
latch.Gen = handle.Gen;
|
||||
latch.ConfigVersion = configVersion;
|
||||
return sizeof(MGPVertexElements) + kAttribBytes + kBindingBytes;
|
||||
}
|
||||
|
||||
Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs> m_attributes{};
|
||||
Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs> m_bindingPoints{};
|
||||
Array<Uint8, kMGPipeMaxVertexAttribs *(sizeof(MGPVertexAttribWire) + sizeof(MGPVertexBindingPointWire))>
|
||||
m_blob{};
|
||||
Array<MGPVertexBuffer, kMGPipeMaxVertexAttribs> m_entries{};
|
||||
|
||||
MGPVertexElements m_lastElements{};
|
||||
MGPVertexBuffers m_lastBuffers{};
|
||||
MGPIndexBuffer m_lastIndex{};
|
||||
|
||||
Vector<Latch> m_latch;
|
||||
MGPipeHandle m_boundHandle = kMGPipeNullHandle;
|
||||
Uint64 m_boundLifetimeId = 0;
|
||||
|
||||
Uint64 m_creates = 0;
|
||||
Uint64 m_binds = 0;
|
||||
Uint64 m_bufferSets = 0;
|
||||
Uint64 m_indexSets = 0;
|
||||
};
|
||||
|
||||
// The monolith's one vertex-input emitter, beside the tracker, the CSO cache, the
|
||||
// set-hash suppressor and the resource tracker.
|
||||
inline MGPipeVertexInputEmitter& MGPipeVertexInputEmitterInstance() {
|
||||
static MGPipeVertexInputEmitter emitter;
|
||||
return emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -302,10 +302,17 @@ namespace {
|
||||
|
||||
// The five P2 emits for each name their own subsystem; the rest name none, which is what
|
||||
// makes MOBILEGL_PIPE_PUSH a per-subsystem A/B instead of one switch.
|
||||
// THE NAME IS P2's AND IT STAYS. A test name is never removed (only added), so this case
|
||||
// keeps the name it was born with and follows the phase constant instead of a literal
|
||||
// five: what it has always asserted is "a bit names a subsystem if and only if this build
|
||||
// emits a call for it", which is the property the emission gate and the residual-fill
|
||||
// skip both rest on. P3a took the vertex-input family over, so the set it compares
|
||||
// against is now kMGPipeDirtyEmittedAtP3a - and a bit that gained an arm without gaining
|
||||
// an emitter, or the reverse, still fails here.
|
||||
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||
const auto bit = static_cast<MGPipeDirty>(i);
|
||||
const Bool emitted = (kMGPipeDirtyEmittedAtP2 & MGPipeDirtyBit(bit)) != 0;
|
||||
const Bool emitted = (kMGPipeDirtyEmittedAtP3a & MGPipeDirtyBit(bit)) != 0;
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
|
||||
}
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
|
||||
@@ -313,6 +320,11 @@ namespace {
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexAttribDefaults),
|
||||
kMGPipeSubsystemVertexAttribDefaults);
|
||||
// P3a's three, one subsystem: an operator who clears bit 8 gets the whole legacy
|
||||
// vertex-input arm rather than two thirds of it.
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements), kMGPipeSubsystemVertexInput);
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers), kMGPipeSubsystemVertexInput);
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer), kMGPipeSubsystemVertexInput);
|
||||
}
|
||||
|
||||
TEST_F(TrackerWalk, TheFirstWalkOnAFreshContextPublishesEverything) {
|
||||
|
||||
Reference in New Issue
Block a user