Skip to content

The Unfortunate Reality

Adam Gorski edited this page Aug 4, 2021 · 8 revisions

XFDraw's shader code has changed after this post. The custom shader overhead is now essentially zero.

The problem with custom shader code

The unfortunate reality is that hard coded performance features will always outperform any custom written shader code. Lets look at why this occurs:

Vignette Shader Example:

This is the code behind the high-performance vignette shader which uses the "advanced" shader support:

struct MultiplyBy
{
	out byte4* outColor;
	in float* inMultiplier;

	void main()
	{
		outColor->R *= *inMultiplier;
		outColor->G *= *inMultiplier;
		outColor->B *= *inMultiplier;
	}
};
(The use of a precomputed vignette buffer allows better performance as the vignette opacity values are only calculated once.)

Note the required pointer read and writes. Luckily this shouldn't be a performance hit with the right C++ compiler. However this issue becomes bigger with how the shader entry point void main() is executed: The DeclareShader(,,) macro just replaces the following text

DeclareShader(MultiplyVignette, MultiplyBy, main)

with

void ExecuteShader(MultiplyBy* ptr)
{
	ptr->main();
}
void* myShader = &ExecuteShader;

Later the shader is registered, but that is irrelevant as that only happens once during startup. The bigger issue revolves around the constant requirement of pointers. Firstly, executing a inlined method by the compiler is far faster than having to first execute a method at a pointer, and the execute another method. The struct main() method can be inlined, however I am not sure if there would be a performance benefit. Secondly, short of editing, compiling and inlining the code into the rendering core, the mass use of pointers is unfortunately required to access multiple framebuffers.

While the reading/writing to pointers is not a huge performance hit, since the hardcoded code also uses framebuffer pointers, the issue arises from the loop that increments all of these pointers:

for (int i = 0; i < iSize; ++i)
{
	*((unsigned char**)(bptr + iInstr[i * 3])) += iInstr[i * 3 + 1];
}

This loop would normally just be a colorBufferPtr+=4; and vingetteBufferPtr++; however XFCore needs to 1. Create the loop. 2. Find the byte position of the next pointer in the struct, and 3. Increment it by the sizeof(data) pointer. These extra 3 steps greatly slowdown the performance of the renderer.

Benchmarks

To put this in perspective here are benchmarks of the custom vignette shader compared to the hardcoded shader:

// The hardcoded shader in GLFast.VignetteMultiply(frameBuffer, vignetteBuffer);
__declspec(dllexport) void VignettePass(long* TargetBuffer, float* SourceBuffer, long Width, long Height)
{
#pragma omp parallel for //Alternatively use the Parallel Patterns Library
	for (int h = 0; h < Height; ++h)
	{
		unsigned char* bptr = (unsigned char*)(TargetBuffer + Width * h);
		float* fptr = SourceBuffer + Width * h;

		for (int w = 0; w < Width; ++w, bptr += 4, ++fptr)
		{
			bptr[0] *= *fptr;
			bptr[1] *= *fptr;
			bptr[2] *= *fptr;
		}
	}	
}
Note that both the Parallel Patterns Library or OpenMP can be used here.

Here is a 1000 iteration benchmark of both performance test for a 1600x900 buffer:

Method Avg. Time (ms)
Hard Coded 0.4823924
Custom Shader 1.6230542
Empty Shader 0.3524819

That's a 3.36X performance improvement. Note how the empty shader uses an entire 0.35ms just as overhead.

Clone this wiki locally