[Fix] (MG_State, MG_Impl): GL-order capture for geometry triangle strips

Vulkan transform feedback captures odd strip triangles as (i, i+2, i+1)
while GL table 10.1 decomposes them as (i+1, i, i+2). When the capture
stage is a triangle-strip geometry shader whose EmitVertex/EndPrimitive
sequence is statically knowable (no emission under control flow), link
time extracts the per-invocation strip lengths from the glslang AST, and
EndTransformFeedback rotates each odd triangle's captured vertex records
into GL order in place (bounded by the binding ranges' whole-triangle
capacity; raw input primitives tracked per Begin/End).
KHR-GL33.transform_feedback.geometry passes - the family is 21/21.
This commit is contained in:
BZLZHH
2026-08-01 03:17:30 -04:00
parent 4407be89cd
commit dd745d7547
4 changed files with 148 additions and 0 deletions
@@ -74,6 +74,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
Uint64 primitives = CountPrimitivesForDraw(mode, count);
if (primitives == 0) return;
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
Uint64 verticesPerPrimitive = 1;
switch (mode) {
@@ -579,6 +580,66 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
}
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
// lengths the captured records are reordered in place: swap the first two
// vertex records of every odd triangle within each emitted strip.
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
Uint64 inputPrimitives) {
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
return;
}
const auto& stripTriangles = program->GetGsStripTriangles();
// Global triangle indices whose leading vertex pair must swap.
Vector<Uint64> swapTriangles;
Uint64 triangleBase = 0;
for (Uint64 input = 0; input < inputPrimitives; ++input) {
for (const Uint32 stripLength : stripTriangles) {
for (Uint32 t = 1; t < stripLength; t += 2) {
swapTriangles.push_back(triangleBase + t);
}
triangleBase += stripLength;
}
}
if (swapTriangles.empty()) {
return;
}
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
if (stride == 0) continue;
const auto& bindingPoint =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(bufferIndex));
const auto& buffer = bindingPoint.GetBoundObject();
if (buffer == nullptr) continue;
const Range1D range = bindingPoint.GetRange();
const Uint8* mapped = buffer->MappedData();
if (mapped == nullptr) continue;
// The geometry stage amplifies, so the CPU vertex counter does not bound
// the capture; the binding range's whole-triangle capacity does.
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
// (winding preserved by swapping the trailing pair); GL wants
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
Vector<Uint8> scratch(stride);
for (const Uint64 triangle : swapTriangles) {
if (triangle >= capturedTriangles) break;
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
const SizeT v1Offset = v0Offset + stride;
const SizeT v2Offset = v1Offset + stride;
Memcpy(scratch.data(), mapped + v2Offset, stride);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
}
}
}
void EndTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
@@ -586,6 +647,8 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
return;
}
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
MG_State::pGLContext->EndTransformFeedback();
// Captured results must be visible to MapBuffer/GetBufferSubData after
// End; the capture targets are host-coherent GPU memory, so completing
@@ -599,6 +662,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
}
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
}
} // namespace MobileGL::MG_Impl::GLImpl
+8
View File
@@ -218,6 +218,7 @@ namespace MobileGL {
m_transformFeedbackProgram = program;
++m_transformFeedbackGeneration;
m_transformFeedbackCapturedVertices = 0;
m_transformFeedbackInputPrimitives = 0;
}
void EndTransformFeedback() {
m_transformFeedbackActive = false;
@@ -244,6 +245,12 @@ namespace MobileGL {
m_transformFeedbackCapturedVertices += vertices;
}
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
// Raw assembled input primitives fed to the capture stage since Begin
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives;
}
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
// Framebuffer
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
@@ -283,6 +290,7 @@ namespace MobileGL {
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
TextureState m_textureState;
ProgramState m_programState;
RenderState m_renderState;
@@ -324,9 +324,76 @@ namespace MobileGL::MG_State::GLState {
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
}
}
ResolveGsTriangleStripCapture(captureIntermediate);
return true;
}
namespace {
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
// the static strip lengths the capture buffer can be reordered after EndTF.
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
public:
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
if (node->getOp() == glslang::EOpEmitVertex) {
++emitCount;
hasEmit = true;
} else if (node->getOp() == glslang::EOpEndPrimitive) {
FlushStrip();
}
return true;
}
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
inControlFlow = true;
return true;
}
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
inControlFlow = true;
return true;
}
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
inControlFlow = true;
return true;
}
void FlushStrip() {
if (emitCount >= 3) {
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
}
emitCount = 0;
}
Vector<Uint32> stripTriangles;
Uint32 emitCount = 0;
Bool hasEmit = false;
Bool inControlFlow = false;
};
} // namespace
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
m_gsStripTriangles.clear();
m_gsStripCaptureFixup = false;
if (captureIntermediate == nullptr || m_program == nullptr) {
return;
}
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
return;
}
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
return;
}
GsEmitSequenceTraverser traverser;
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
return;
}
m_gsStripTriangles = Move(traverser.stripTriangles);
m_gsStripCaptureFixup = true;
}
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
@@ -491,6 +491,12 @@ namespace MobileGL::MG_State::GLState {
}
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
// True when the capture stage is a triangle-strip geometry shader with a
// statically-known emit sequence: the Vulkan capture order then needs the GL
// odd-triangle vertex swap after EndTransformFeedback.
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
// Triangles per strip, in emission order, for ONE geometry invocation.
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
@@ -506,6 +512,7 @@ namespace MobileGL::MG_State::GLState {
// vertex stage; fails the link (GL semantics) on unknown or duplicate
// names or exceeded capture limits.
Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
void GenerateBinary();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing();
@@ -595,6 +602,8 @@ namespace MobileGL::MG_State::GLState {
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Vector<XfbVarying> m_xfbVaryings;
Vector<Uint32> m_xfbStrides;
Vector<Uint32> m_gsStripTriangles;
Bool m_gsStripCaptureFixup = false;
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int m_xfbVaryingNameMaxLength = 0;
};