-
Notifications
You must be signed in to change notification settings - Fork 0
The Unfortunate Reality
XFDraw's shader code has changed after this post. The custom shader overhead is now essentially zero.
The unfortunate reality is that hard coded performance features will always outperform any custom written shader code. Lets look at why this occurs:
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.
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;
}
}
}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.