Inconsistent behavior of Transform Feedback Object

Hi,
I’m trying to use Transform Feedback with Geometry Shader to modify vertex number to be rendered. glDrawTransformFeedback and two transform feedback objects are used to draw the number of vertices captured after geometry shader.

The test program is simplified as

  1. feed a VBO with single float data with initial value (10.0f)
  2. in geometry shader, increase the value by 1, emit vertex only if the value is less than (12.0f).
    Those numbers above are arbitrary.
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"

using namespace ci;
using namespace ci::app;
using namespace std;

static const std::string vertexSrc = R"END(
#version 330 core

layout (location = 0)
in float inValue;

out float vsValue;

void main()
{
	vsValue = inValue;
}
)END";

static const std::string geomSrc = R"END(
#version 330 core

layout (points) in;
layout (points, max_vertices = 1) out;

in float vsValue[];

out float outValue;

void main()
{
	if (vsValue[0] < 12)
	{
		outValue = vsValue[0] + 1;
		
		EmitVertex();
		EndPrimitive();
	}
}
)END";

class TestTransformFeedbackObjectApp : public App {
public:
	void setup() override;
	void update() override;
	void draw() override;
	
	void loadBuffers();
	void loadShaders();
	void printBuffers();
	
	gl::VaoRef mVao[2];
	gl::TransformFeedbackObjRef mTfo[2];
	gl::VboRef mVbo[2];
	// 0 or 1, used as index accesing above buffers
	int mSourceIndex;
	bool bFirstTime;
	
	gl::GlslProgRef mFeedbackGlsl;
	gl::QueryRef mQuery;
};

void TestTransformFeedbackObjectApp::loadBuffers()
{
	float data[] = { 10.0f };
	
	for (int i = 0; i < 2; ++i)
	{
		mVao[i] = gl::Vao::create();
		mVao[i]->bind();
		
		if (i == 0)
		{
			mVbo[i] = gl::Vbo::create(GL_ARRAY_BUFFER, sizeof(data), data);
		}
		else
		{
			mVbo[i] = gl::Vbo::create(GL_ARRAY_BUFFER, sizeof(data), nullptr);
		}
		mVbo[i]->bind();
		gl::vertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(GL_FLOAT), (const GLvoid*)0);
		gl::enableVertexAttribArray(0);
		mVbo[i]->unbind();
		
		mVao[i]->unbind();
		
		mTfo[i] = gl::TransformFeedbackObj::create();
		mTfo[i]->bind();
		gl::bindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, mVbo[i]);
		mTfo[i]->unbind();
	}
	
	mSourceIndex = 0;
}

void TestTransformFeedbackObjectApp::loadShaders()
{
	gl::GlslProg::Format format;
	format.vertex(vertexSrc).geometry(geomSrc).feedbackVaryings({ "outValue" })
	.feedbackFormat(GL_INTERLEAVED_ATTRIBS);
	
	mFeedbackGlsl = gl::GlslProg::create(format);
}

void TestTransformFeedbackObjectApp::printBuffers()
{
	GLfloat data;
	mVbo[mSourceIndex]->getBufferSubData(0, sizeof(data), &data);
	console() << "  Source Index: " << mSourceIndex << std::endl;
	console() << "    data: " << data << std::endl;
	
	mVbo[1 - mSourceIndex]->getBufferSubData(0, sizeof(data), &data);
	console() << "  Target Index: " << 1 - mSourceIndex << std::endl;
	console() << "    data: " << data << std::endl;
}

void TestTransformFeedbackObjectApp::setup()
{
	loadBuffers();
	loadShaders();
	
	bFirstTime = true;
	mQuery = gl::Query::create(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
}

void TestTransformFeedbackObjectApp::update()
{
	gl::ScopedGlslProg scpGlsl(mFeedbackGlsl);
	gl::ScopedState scpState(GL_RASTERIZER_DISCARD, true);
	
	// transform feedback source
	gl::ScopedVao scpVao(mVao[mSourceIndex]);
	// transform feedback target
	mTfo[1 - mSourceIndex]->bind();
	
	console() << "\n[Before feedback]: " << std::endl;
	printBuffers();
	
	mQuery->begin();
	
	gl::beginTransformFeedback(GL_POINTS);
	if (bFirstTime)
	{
		gl::drawArrays(GL_POINTS, 0, 1);
		bFirstTime = false;
	}
	else
	{
		glDrawTransformFeedback(GL_POINTS, mTfo[mSourceIndex]->getId());
	}
	gl::endTransformFeedback();
	
	mQuery->end();
	
	console() << "** Primitive written: " << mQuery->getValueUInt() << std::endl;
	
	console() << "[After feedback]: " << std::endl;
	printBuffers();
	
	mSourceIndex = 1 - mSourceIndex;
}

void TestTransformFeedbackObjectApp::draw()
{
	gl::clear(Color(0, 0, 0));
}

CINDER_APP(TestTransformFeedbackObjectApp, RendererGl)


This test program output differently on two platform,

  1. Window 10, NVidia GTX1050, OpenGL 4.5
  2. macOS, NVdia GTM 650, OpenGL 4.1

Theoretically, the GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN value should be 1 for two times, as the transform varying value exceeds . After that, it should be 0.

On Windows machine, the program works as intended, which is not the case on macOS, it seems that glDrawTransformFeedback did not derived the the number of vertices from the transform feedback object.

I’m wondering if this is OpenGL implementation related or I just missed something in the code. Thank you!

Interesting findings.

I haven’t run your code so I might be totally wrong.
But my guess is that, when the value is bigger than 12,
the number of data generated from the geometry shader would be inconsistent with the number of data in vbo,
which would lead to undefined behaviours.

Could you try the following geometry shader? And see if it runs fine on a mac?

#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;

in float vsValue[];

out float outValue;

void main()
{
	outValue = vsValue[0] + step(vsValue[0], 12);
    EmitVertex();
	EndPrimitive();
}

Thank you seph,
I just tried your suggestion, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN is always 1, as this geometry shader emit vertex all the time. However, I would like to stop emit vertex in some conditions as a way to ‘destroy’ vertices.

Here is the output of the test program on macOS


[Before feedback]: 
  Source Index: 0
    data: 10
  Target Index: 1
    data: 0
** Primitive written: 1
[After feedback]: 
  Source Index: 0
    data: 10
  Target Index: 1
    data: 11

[Before feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 10
** Primitive written: 1
[After feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 12

[Before feedback]: 
  Source Index: 0
    data: 12
  Target Index: 1
    data: 11
** Primitive written: 0
[After feedback]: 
  Source Index: 0
    data: 12
  Target Index: 1
    data: 11

[Before feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 12
** Primitive written: 1
[After feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 12

[Before feedback]: 
  Source Index: 0
    data: 12
  Target Index: 1
    data: 11
** Primitive written: 0
[After feedback]: 
  Source Index: 0
    data: 12
  Target Index: 1
    data: 11

[Before feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 12
** Primitive written: 1
[After feedback]: 
  Source Index: 1
    data: 11
  Target Index: 0
    data: 12

From the output, I could tell that if geometry shader did not emit vertex, the data in VBO will be left without touch, the number of vertex in the bound transform feedback object would be 0.

However, in the next iteration, glDrawTransformFeedback seems ignore the vertex number in transform feedback object, and draw 1 vertex again. As the data value in VBO pass test in geometry shader, so the GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN becomes 1 again. After that, we can see this value keeps toggling between 1 and 0.

On Windows machine, as long as GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN becomes 0, it will stay at that value.

Ah I see what you mean now.

openGL wiki only says that if bound data is smaller than recorded data then undefined behaviour will occur. But very likely the other way around will also issue an undefined behaviour too. Maybe you could try checking glError?

To have a stable output my suggestion is to always emit same amount of data as the vbo holds.
If your goal is to have a ‘switch’ on the data in this vbo buffer, you could simply do something like:

if(data > 12.0 || data < 0.0){
    outValue = -1.0;
} else {
    outValue = vsValue[0] + 1.0;
}
EmitVertex();
EndPrimitive();

that the output value will stay with -1.0 and you could use -1.0 as a ‘checking switch’.

Thank you for your reply!

Yes, I did check glError but did not get anything.

From OpenGL wiki you’ve pointed out, do you mean

Undefined behavior results if your bound buffer ranges are not large enough to hold the recorded data.

However, in the test program, the bounded VBO has an ‘allocated’ space for 1 vertex, and the output from geometry shader is 1 or 0 vertex, wouldn’t it be enough to hold the recorded data?

I’ll take your suggestion of the ‘checking switch’ for the current project, thank you!