[Fix] (DirectVulkan, MG_State): give each transform feedback object its own capture counters

The frontend half of ARB_transform_feedback2 landed for both backends, but Magma's
capture was still written for the one implicit span GL 3.3 has:

- A paused span kept capturing. VK_EXT_transform_feedback's counter buffers already
  make consecutive draws append, so pausing is simply "do not wrap this draw" - the
  counters keep their values and the next resumed draw carries on where the last
  captured one stopped.
- Those counter buffers were context-wide. Transform feedback objects can each hold an
  open, paused span at the same time - KHR-GL40.transform_feedback.draw_xfb_test keeps
  three - and they were all appending through one set of four slots. Each object now
  gets its own group, handed out on first use; past sixteen objects they share group 0,
  which only matters for concurrently-paused spans.
- The generation that identifies a span is what a backend keys its append state on, so
  it is now part of the per-object state the frontend saves and restores. Without that,
  resuming an object that was paused before another one began looked like a new span
  and restarted its counters at zero.

GL_PRIMITIVES_GENERATED needed one more thing. It counts what the last vertex
processing stage emitted whether or not anything is being captured, but
VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT only counts what the capture saw - so a
draw made while the span was paused is invisible to it. The frontend now tallies those
draws, and the Vulkan query adds the delta at result time. The correction lives in the
backend that needs it: an ES driver's GL_PRIMITIVES_GENERATED counts them by itself, and
adding it there too would double them.

transform_feedback* on Magma: 4 failures -> 3. Espryt stays at 38/38.
This commit is contained in:
BZLZHH
2026-08-04 11:42:44 -04:00
parent 38497174c8
commit fd29cb914e
6 changed files with 78 additions and 13 deletions
@@ -1578,6 +1578,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (and, via the SharedPtrs, the records), never pool slots, so
// stale queries are always safe to delete.
Uint64 rendererGeneration = 0;
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
// capture saw, so a draw made while the span was paused is invisible to it -
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
// emitted regardless. The delta closes that gap at result time.
Uint64 pausedPrimitiveSnapshot = 0;
};
} // namespace
@@ -1676,6 +1682,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
primitives)) {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
*outNanoseconds = primitives;
return true;
}
@@ -1719,6 +1729,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* query = new VulkanTimerQuery{};
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot =
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
return query;
}
@@ -2798,6 +2798,10 @@ void main() {
}
m_vertexInputStateFactory.reset();
m_xfbCounterBuffer.Destroy();
m_xfbCounterSlotByObject.clear();
m_xfbNextCounterSlot = 0;
m_xfbCountersValid.fill(false);
m_xfbLastSeenGeneration.fill(0);
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, m_occlusionQueryPool, nullptr);
m_occlusionQueryPool = VK_NULL_HANDLE;
@@ -8000,11 +8004,30 @@ void main() {
resource->layout = finalLayout;
}
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;
}
// 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;
return slot;
}
Bool VulkanRenderer::BeginXfbCaptureForDraw(FrameContext::FrameData& frame) {
if (!m_transformFeedbackFeatureEnabled || MG_State::pGLContext == nullptr ||
!MG_State::pGLContext->IsTransformFeedbackActive()) {
return false;
}
// A paused span captures nothing, and the counter buffers keep their values, so the
// next resumed draw appends exactly where the last captured one stopped - which is
// what pause/resume means (ARB_transform_feedback2).
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
return false;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
return false;
@@ -8017,7 +8040,7 @@ void main() {
if (!m_xfbCounterBuffer.IsValid()) {
if (!m_xfbCounterBuffer.Create({
.allocator = m_allocator,
.size = 16,
.size = 16 * kXfbCounterObjectSlots,
.usage = VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
@@ -8058,15 +8081,16 @@ void main() {
s_vkCmdBindTransformFeedbackBuffersEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), buffers,
offsets, sizes);
const Uint32 counterSlot = CurrentXfbCounterSlot();
const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration();
const Bool resume = m_xfbCountersValid && m_xfbLastSeenGeneration == generation;
m_xfbLastSeenGeneration = generation;
const Bool resume = m_xfbCountersValid[counterSlot] && m_xfbLastSeenGeneration[counterSlot] == generation;
m_xfbLastSeenGeneration[counterSlot] = generation;
VkBuffer counterBuffers[4] = {};
VkDeviceSize counterOffsets[4] = {};
for (SizeT i = 0; i < bufferCount; ++i) {
counterBuffers[i] = m_xfbCounterBuffer.GetHandle();
counterOffsets[i] = static_cast<VkDeviceSize>(i) * 4;
counterOffsets[i] = static_cast<VkDeviceSize>(counterSlot) * 16 + static_cast<VkDeviceSize>(i) * 4;
}
if (resume) {
s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount),
@@ -8083,15 +8107,16 @@ void main() {
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
const SizeT bufferCount = program ? std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4) : 0;
const Uint32 counterSlot = CurrentXfbCounterSlot();
VkBuffer counterBuffers[4] = {};
VkDeviceSize counterOffsets[4] = {};
for (SizeT i = 0; i < bufferCount; ++i) {
counterBuffers[i] = m_xfbCounterBuffer.GetHandle();
counterOffsets[i] = static_cast<VkDeviceSize>(i) * 4;
counterOffsets[i] = static_cast<VkDeviceSize>(counterSlot) * 16 + static_cast<VkDeviceSize>(i) * 4;
}
s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), counterBuffers,
counterOffsets);
m_xfbCountersValid = true;
m_xfbCountersValid[counterSlot] = true;
}
void VulkanRenderer::DrawArrays(const DrawCmd& payload) {
@@ -498,12 +498,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
// Counter buffers (one 4-byte slot per capture binding) let consecutive
// draws within one glBeginTransformFeedback append GL-style.
// draws within one glBeginTransformFeedback append GL-style. Transform feedback
// objects can each hold an open, paused span at the same time, so the counters are
// per object: one group of four slots each, handed out on first use.
static constexpr SizeT kXfbCounterObjectSlots = 16;
VkBufferObject m_xfbCounterBuffer;
// Non-zero while inside a GL Begin/End with at least one captured draw
// recorded; selects counter-buffer resume on the next captured draw.
Bool m_xfbCountersValid = false;
Uint64 m_xfbLastSeenGeneration = 0;
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
Uint32 m_xfbNextCounterSlot = 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{};
Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
// Counter slot group of the bound transform feedback object.
Uint32 CurrentXfbCounterSlot();
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
// when GL transform feedback is active; binds capture buffers on demand.
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
@@ -75,7 +75,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
// A paused span captures nothing, so a draw made while paused contributes to
// PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
if (MG_State::pGLContext->IsTransformFeedbackPaused()) return;
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count));
return;
}
Uint64 primitives = CountPrimitivesForDraw(mode, count);
if (primitives == 0) return;
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
+5
View File
@@ -764,6 +764,7 @@ namespace MobileGL::MG_State {
object.paused = m_transformFeedbackPaused;
object.primitiveMode = m_transformFeedbackPrimitiveMode;
object.program = m_transformFeedbackProgram;
object.generation = m_transformFeedbackGeneration;
object.capturedVertices = m_transformFeedbackCapturedVertices;
object.inputPrimitives = m_transformFeedbackInputPrimitives;
}
@@ -783,6 +784,10 @@ namespace MobileGL::MG_State {
m_transformFeedbackPaused = object.paused;
m_transformFeedbackPrimitiveMode = object.primitiveMode;
m_transformFeedbackProgram = object.program;
// The generation identifies one capture span, and a span belongs to the object
// that opened it - a backend keys its append state on it, so switching objects
// has to bring the right one back.
m_transformFeedbackGeneration = object.generation;
m_transformFeedbackCapturedVertices = object.capturedVertices;
m_transformFeedbackInputPrimitives = object.inputPrimitives;
}
+14 -1
View File
@@ -221,7 +221,7 @@ namespace MobileGL {
m_transformFeedbackPaused = false;
m_transformFeedbackPrimitiveMode = primitiveMode;
m_transformFeedbackProgram = program;
++m_transformFeedbackGeneration;
m_transformFeedbackGeneration = ++m_transformFeedbackNextGeneration;
m_transformFeedbackCapturedVertices = 0;
m_transformFeedbackInputPrimitives = 0;
}
@@ -251,6 +251,15 @@ namespace MobileGL {
m_transformFeedbackPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
// Primitives a draw assembled while the capture was paused. GL counts those in
// PRIMITIVES_GENERATED, but a backend that answers the query with its own
// transform feedback counter cannot see them - nothing was being captured.
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
m_transformFeedbackPausedPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
return m_transformFeedbackPausedPrimitiveCounter;
}
// Vertices already captured since BeginTransformFeedback (drives the
// buffer-capacity clamp on the primitives-written accounting).
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
@@ -327,9 +336,12 @@ namespace MobileGL {
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
Uint64 m_transformFeedbackGeneration = 0;
// Source of the per-span ids above; never rolls back with an object switch.
Uint64 m_transformFeedbackNextGeneration = 0;
// Not object state: the transform feedback queries snapshot it at BeginQuery
// and take the delta at EndQuery, which spans whatever objects were used.
Uint64 m_transformFeedbackPrimitiveCounter = 0;
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
@@ -345,6 +357,7 @@ namespace MobileGL {
Bool paused = false;
GLenum primitiveMode = GL_POINTS;
SharedPtr<ProgramObject> program;
Uint64 generation = 0;
Uint64 capturedVertices = 0;
Uint64 inputPrimitives = 0;
Uint64 recordedVertices = 0;