Skip to main content
Up and Coming
April 11, 2013

Shader Optimization.

I was playing with the shader and wondered if it could be optimized.

Lets start with the standard Shader


float2 HmdWarp(float2 in01)
{
float2 theta = (in01 - LensCenter) * ScaleIn; // Scales to [-1, 1]
float rSq= theta.x * theta.x + theta.y * theta.y;
float2 theta1 = theta * (HmdWarpParam.x + HmdWarpParam.y * rSq +
HmdWarpParam.z * rSq * rSq + HmdWarpParam.w * rSq * rSq * rSq);
return LensCenter + Scale * theta1;
}

float4 main( float2 texCoord : TEXCOORD0,float4 color : COLOR0 ) : SV_Target
{
float2 tc = HmdWarp(texCoord);
if (any(clamp(tc, ScreenCenter-float2(0.25,0.5), ScreenCenter+float2(0.25, 0.5)) - tc))
return 0;
return Texture.Sample(Linear, tc);

};


That's 11 Multiplies, 6 Additions and 3 Subtractions per pixel. (eyeballed the numbers sorry, could be off by one or two) and on my GTX670 takes 0.1447264 milliseconds to execute (based on averaging the time of a 100 frames of just the draw with the shader)

Which could be good, could be bad, nothing to compare it to, so lets compare it to a shader that does not warp and just samples the texture.


float4 pixelShader( float2 texCoord : TEXCOORD0,float4 color : COLOR0 ) : SV_Target
{
float4 c = Texture.Sample(Linear, texCoord);
return c;
}


which takes 0.0272857 milliseconds to execute (again GTX670, 100 frames)

So the warping shader is 5.3 times slower than just bare sampling.

There must be a better way, why are we recalculating the distortion map every frame single frame? seems rather wasteful. Lets render the distortion map to texture instead. We'll use the Red channel for the X distortion, and the Green channel for the Y distortion. If we make a texture of format R32G32_Float it should fit our needs nicely. And the upside is we only have to do the nasty math once!

our shader now looks like this


float2 HmdWarp(float2 in01)
{
/// Same as above, cut to keep the post size down.
}

float4 main( float2 texCoord : TEXCOORD0,float4 color : COLOR0 ) : SV_Target
{
float2 tc = HmdWarp(texCoord);
if (any(clamp(tc, ScreenCenter-float2(0.25,0.5), ScreenCenter+float2(0.25, 0.5)) - tc))
return 0;
return float4(tc.x, tc.y,0,0);
};


The resulting texture will look kinda like this (Used bad parameters, had to guess since I don't have a rift yet so the horizontal mapping is somewhat off)



Sweet, almost in business! lets update the warping shader to make use of the new texture. First of all the linear sampler has to go, a MinMagMipPoint sampler will fit our needs better.


SamplerState PointSampler : register(s0);
Texture2D Texture : register(t0);
Texture2D TextureWarp : register(t1);

float4 main( float2 texCoord : TEXCOORD0,float4 color : COLOR0 ) : SV_Target
{
float4 c = TextureWarp.Sample(PointSampler, texCoord);
float2 RealCoord = float2(c.r,c.g);
return Texture.Sample(PointSampler, RealCoord);
};


Ahh much simpler now! New execution time 0.0598547 milliseconds and no visual difference! 2.5 times speed up from the original shader! Not bad for 20 minutes of tinkering.

    80 replies

    Up and Coming
    April 22, 2013
    "lazydodo" wrote:
    Yeah it's a neat optimalisation trick but it should be taken within context,
    the overal improvement on the shader time is great (up to 3.8 times!) but
    relative to the complete frametime the improvement is not nearly as massive.



    That's very true, but I did a rough test of the Unity tuscany demo on a really slow system (averages 20fps) and the effect of the warp effect being on vs off altogether was about 2-3 fps, or more than 10% of performance.

    Obviously, that's meaningless when the framerate is that low to begin with... But... At those framerates it shows it's not a triviality... XD.
    April 22, 2013
    "lazydodo" wrote:
    Yeah it's a neat optimalisation trick but it should be taken within context,
    the overal improvement on the shader time is great (up to 3.8 times!) but
    relative to the complete frametime the improvement is not nearly as massive.

    I'm not too concerned about overall speed, just curious as to why the GLSL shader is about 15% faster when recomputing rather than sampling on my GTX460. I'll see if I get the same results in HLSL sometime, but my Rift arrived today so I have a long list of other things to do first.
    Up and Coming
    April 22, 2013
    Hitting a consistent 60fps in a real game is hard, even for relatively simple games. Every tiny bit matters. And 1ms is actually pretty huge. Proportionally its like finding an extra hour of free time in your day, every day.

    You shouldn't think of it in terms of FPS gained, because that is dependent on other factors its not a useful measure for optimization. Use GPU profilers and hardware queries if you can.
    Up and Coming
    April 23, 2013
    So as someone new to shaders... do we just use the above example code to make a custom copy of the included shaders? And build the texture map?
    New Participant
    April 24, 2013
    The speed up is impressive from using the UVoffset map but the original shader should not be that slow. The GTX 670 is a beast of a card and you have to have a large shader before you are ALU bound rather than memory bound.

    I made a few optimisations to the warp function and it runs at exactly the same rate as not doing a warp at all.

    What I'm figuring is the speed up is not coming from lower ALU usage but instead from better memory access patterns.

    The big change I made to my own engine is to not render to a double wide render target for each eye but to a single wide target (i.e. 640x800 *1.75). The big advantage of this is you can set BORDER TADDRESS mode with black borders and remove the if() statment to check inside viewport part of the warp. This cuts down ALU usage but more important cuts out texture reads outside the texture. If you do the if() branch the texture read will still happen but the results then ignored. I think this may be the main culprit for the performance slowdown.
    Up and Coming
    April 24, 2013
    Low-Level Thinking in High-Level Shading Languages (from GDC 2013):
    http://www.humus.name/Articles/Persson_LowLevelThinking.pdf
    Optimize all the shaders!
    This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
    Up and Coming
    April 24, 2013
    There is really no need for a conditional in the LUT based shader. Just have your distortion LUT cover both eyes, over the entire screen, and your final distortion shader is a single straightforward dependent lookup with no other logic.
    lazydodoAuthor
    Up and Coming
    April 25, 2013
    Does anyone else have timing information? I switched to a different test case given my rift came in (wrote a quick hack to display the tuscany data in C#) and with that came a resolution bump from windowed 800x480 with the old test code to 1920x1080 full screen and fixing some of the wrong parameters i had guessed for the warp shader. (it all looks ok now in the rift) and I'm getting the following new timings on the 670: (Loads measured with GPU-Z)


    Warp Shader : 2.07178596721311 GPULoad 25% MemoryController Load: 11%
    Just Sample : 0.20670740983606 GPULoad 15% MemoryController Load: 7% Speedup: 10.0x
    TextureLookup R16G16: 0.29586780327868 GPULoad 15% MemoryControlelr Load: 8% Speedup: 7.0x
    TextureLookup R32G32: 0.32191370491803 GPULoad 15% MemoryController Load: 8% Speedup: 6.4X


    The timings are also slightly different than my previous numbers, previously it was a single run for the warp shader (but it needs to run twice to render both eyes) now all timings are the time needed to completely distort both eyes (but excluding the render time for the frame). Also the TextureLookup Shader has been modified to cache both eyes in a single texture so the texture now looks like this



    Now what strikes me as odd is does the warp shader really add 2ms / frame at 1920x1080? Seems... high, can someone reproduce/validate?

    Also aliasbinman care to post your optimized shader? Love to give it a whirl
    April 25, 2013
    I don't have time to set up a proper comparison this morning, but at 1920x1080 with no mipmapping and 16 samples per pixel, the GLSL version of the original shader only takes 0.31ms on my GTX460.
    New Participant
    April 25, 2013
    Here is the hlsl

    float2 BarrelWarp(float2 UV)
    {
    //return UV;// Use to test no warp

    UV = (UV * OVR_ReScaleUV.xy) + OVR_ReScaleUV.zw;

    float4 rSq;
    rSq.x = 1;
    rSq.y = dot(UV,UV);
    rSq.z = rSq.y * rSq.y;
    rSq.w= rSq.y * rSq.z;

    float w = dot(rSq, OVR_Warp.xyzw);

    UV = OVR_LensCentre.xy + (UV * w * OVR_Scale.xy);
    return UV;
    }

    The OVR_RescaleUV is a combination of ScaleIn and LensCentre done on the CPU
    D3DXVECTOR4(vrScaleIn.x, vrScaleIn.y, vrScaleIn.x * -vrLensCenter.x , vrScaleIn.y * -vrLensCenter.y);