mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 05:38:31 +09:00
[Fix] (DirectVulkan, State): key the transform-feedback counter slots on the object's identity, not on its recycled GL name
- VulkanRenderer::CurrentXfbCounterSlot keyed m_xfbCounterSlotByObject on
GetBoundTransformFeedbackName(). glGenTransformFeedbacks hands a deleted name
straight back (IndexGenerator is LIFO) and nothing ever removed a map entry, so
a transform feedback object created on a recycled name was served the DEAD
object's counter group - and with it that group's m_xfbCountersValid and
m_xfbLastSeenGeneration entries, which are the resume/fresh decision for
vkCmdBeginTransformFeedbackEXT. This is D21 in plan B v2 4.7.3, the one entry
in that table whose today-key "guards nothing", and 10.4-5 asks for it to land
on dev on its own - hence this separate commit, kept in files no other commit
on this branch touches so the cherry-pick applies unaided.
- Frontend: TransformFeedbackObjectState gains a never-reused `lifetimeId`
through a default member initialiser, so every route into existence
(operator[] materialisation, `= {}` in GenTransformFeedbackNames and
CreateTransformFeedbackObject) mints a fresh one and a recycled name cannot
carry the dead object's id back. The allocator is the same shape as
BufferObject::AllocateLifetimeId (atomic, starts at 1 so a zeroed backend slot
is never a live object).
- The bound object's id is mirrored in m_boundTransformFeedbackLifetimeId,
refreshed by RestoreBoundTransformFeedbackState - which every bind, and the
revert that deleting the bound object performs, goes through - and seeded for
the default object by the GLContext constructor. GetBoundTransformFeedback
LifetimeId is therefore a const load. Reading it through operator[] instead
would have been an INSERT on the per-draw path, and UnorderedMap is
ska::flat_hash_map, whose rehash invalidates every reference into the
container, not just its iterators.
- Backend: the UnorderedMap is replaced by a fixed 16-entry owner table, which
fixes the second half of the same defect - the map was keyed on a value that
recycles yet was never pruned, so it grew for the life of the context. With
lifetime ids as keys a map would have grown without bound instead, so the
bounded table is required, not cosmetic.
- Slot exhaustion: past sixteen owners a group has to be taken over, and the
victim is chosen among owners with NO OPEN SPAN, which
GLContext::HasOpenTransformFeedbackSpan answers; an identity no live object
carries any more answers false, and that is what lets a dead owner's group
come back. Least-recently-used ALONE would have been exactly the wrong rule:
GL only permits another object to capture while this one is PAUSED, so the
paused span these groups exist to protect is by construction the least
recently used entry, and an LRU takeover would reset the one resume offset
that still matters. LRU is now only the tie-break among reclaimable groups.
Sixteen genuinely open spans at once is reported (MGLOG_E_ONCE) rather than
resolved silently, because whatever is taken then restarts at offset 0.
- Not done, and why: the natural place to hand a group back is
glEndTransformFeedback, but registering DirectVulkan's EndTransformFeedback
table entry would flip the test FixupGsStripCaptureOrder makes of that same
pointer (GL_Drawing.cpp:1255) to decide whether the backend already captured
in GL's vertex order, silently disabling the geometry-stage strip fixup for
DirectVulkan. Giving that discriminator a name of its own is a separate
change; until then the no-open-span rule is what keeps the table honest.
- CurrentXfbCounterSlot asserts the identity is never 0. Zero is the free-slot
sentinel, so an identity of 0 would match every free slot as "mine" without
ever claiming one - this bug reintroduced, with no symptom at the call site.
- TransformFeedbackLifetimeIdTest, in its own translation unit, pins the
frontend halves: an object created on a recycled name must not report the dead
object's id, the default object has an identity before anything binds it, and
a PAUSED span still reads as open while another object is bound and capturing
- which is the whole correctness argument for the eviction rule. The name
reuse is not simulated: the test asks the real generator and skips (loudly) if
it never recycled. Still untested: the >16-owners path itself, which needs a
backend scenario with seventeen capturing objects and there is none.
- Negative controls, each applied then reverted: making a non-bound object's
span read as closed reddens APausedSpanStaysOpenWhileAnotherObjectCaptures;
making a vanished identity read as open reddens the same case on its delete
assertion; dropping the constructor's seeding reddens
AnObjectAtARecycledNameCarriesAFreshLifetimeId.
- Tested: cmake --build build-linux -j 24 (clean, 166 targets); ctest -L unit
-j 12 -> 1386/1386 passed; ctest -L integration-gpu -> 866/866 passed
serially, and 866/866 on one of two -j 8 runs. The other -j 8 run failed
DirectGLES.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnviron
mentPinsItOn, a member of the pre-existing parallel-ctest flake family: it
passes in isolation here, and the unmodified parent tree
(~/w7/p0-noop-wins-base) reproduces the same family under -j 8.
This commit is contained in:
@@ -3267,8 +3267,9 @@ void main() {
|
||||
}
|
||||
m_vertexInputStateFactory.reset();
|
||||
m_xfbCounterBuffer.Destroy();
|
||||
m_xfbCounterSlotByObject.clear();
|
||||
m_xfbNextCounterSlot = 0;
|
||||
m_xfbCounterSlotOwner.fill(0);
|
||||
m_xfbCounterSlotLastUse.fill(0);
|
||||
m_xfbCounterSlotUseSerial = 0;
|
||||
m_xfbCountersValid.fill(false);
|
||||
m_xfbLastSeenGeneration.fill(0);
|
||||
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
|
||||
@@ -11199,16 +11200,66 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Keyed on the frontend's never-reused lifetime id, NOT on the GL name. The name is
|
||||
// recycled the moment glDeleteTransformFeedbacks gives it back, so a name-keyed slot
|
||||
// handed a brand-new object the counter group - and the m_xfbCountersValid /
|
||||
// m_xfbLastSeenGeneration entries - of the object that died under that name.
|
||||
//
|
||||
// Slots are never handed back (there is no backend entry telling this renderer that a span
|
||||
// closed - registering the EndTransformFeedback one would flip the "captures through its own
|
||||
// driver" test FixupGsStripCaptureOrder makes of it), so once all sixteen are owned a new
|
||||
// object has to take one over. The victim is chosen among owners with NO OPEN SPAN: an object
|
||||
// whose span is closed, or which no longer exists at all, can never resume, so its counter
|
||||
// bytes are dead. Least-recently-used ALONE would be exactly the wrong rule - GL only permits
|
||||
// another object to capture while this one is PAUSED, so the paused span whose counters the
|
||||
// slots exist to protect is by construction the least recently used entry. Taking a group over
|
||||
// resets its counter state, because those bytes describe the previous owner's span.
|
||||
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
|
||||
const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();
|
||||
const auto it = m_xfbCounterSlotByObject.find(name);
|
||||
if (it != m_xfbCounterSlotByObject.end()) {
|
||||
return it->second;
|
||||
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
|
||||
const Uint64 identity = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
|
||||
MOBILEGL_ASSERT(identity != 0,
|
||||
"transform feedback object reported the free-slot sentinel (0) as its identity - "
|
||||
"every slot would then read as 'mine' without ever being claimed");
|
||||
Uint32 freeSlot = kNoSlot;
|
||||
for (Uint32 slot = 0; slot < kNoSlot; ++slot) {
|
||||
if (m_xfbCounterSlotOwner[slot] == identity) {
|
||||
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
|
||||
return slot;
|
||||
}
|
||||
if (m_xfbCounterSlotOwner[slot] == 0 && freeSlot == kNoSlot) {
|
||||
freeSlot = slot;
|
||||
}
|
||||
}
|
||||
// Past the tracked set every object shares slot group 0. Only concurrently-paused
|
||||
// spans need distinct groups, and applications do not keep sixteen of those open.
|
||||
const Uint32 slot = m_xfbNextCounterSlot < kXfbCounterObjectSlots ? m_xfbNextCounterSlot++ : 0;
|
||||
m_xfbCounterSlotByObject[name] = slot;
|
||||
Uint32 slot = freeSlot;
|
||||
if (slot == kNoSlot) {
|
||||
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
|
||||
if (MG_State::pGLContext->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
|
||||
continue;
|
||||
}
|
||||
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
|
||||
slot = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slot == kNoSlot) {
|
||||
// Sixteen capture spans open at once. Whatever is taken loses its resume offset and
|
||||
// restarts at byte 0 of its capture buffers, which is a wrong picture rather than a
|
||||
// slow one - hence a report rather than a silent choice.
|
||||
MGLOG_E_ONCE("CurrentXfbCounterSlot: all %zu counter groups belong to transform feedback objects "
|
||||
"with an open capture span; the least recently used one is taken over and that span "
|
||||
"will restart at offset 0 instead of appending",
|
||||
kXfbCounterObjectSlots);
|
||||
slot = 0;
|
||||
for (Uint32 candidate = 1; candidate < kNoSlot; ++candidate) {
|
||||
if (m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
|
||||
slot = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_xfbCounterSlotOwner[slot] = identity;
|
||||
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
|
||||
m_xfbCountersValid[slot] = false;
|
||||
m_xfbLastSeenGeneration[slot] = 0;
|
||||
return slot;
|
||||
}
|
||||
|
||||
|
||||
@@ -675,8 +675,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// per object: one group of four slots each, handed out on first use.
|
||||
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||
Uint32 m_xfbNextCounterSlot = 0;
|
||||
// Which transform feedback object owns each slot group, by the frontend's never-reused
|
||||
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
|
||||
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
|
||||
// inherited the dead one's slot, and since nothing ever removed an entry the map also
|
||||
// grew for the life of the context. A fixed table cannot do either: a group is taken over
|
||||
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
|
||||
// counters can still be resumed never loses them, and a dead object's group comes back.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
|
||||
// Tie-break among reclaimable groups only; never on its own, because the paused span the
|
||||
// groups exist for is by construction the least recently used one.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
|
||||
Uint64 m_xfbCounterSlotUseSerial = 0;
|
||||
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||
// counter-buffer resume on the next captured draw of the same span.
|
||||
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||
|
||||
Reference in New Issue
Block a user