10
\$\begingroup\$

I'm trying to do hardware instancing but I'm hitting some strange performance problem. The average framerate is around 45, but it's extremely choppy.

  • Windowed
  • SynchronizeWithVerticalRetrace = false
  • IsFixedTimeStep = false
  • PresentationInterval = PresentInterval.Immediate

The image below shows my measured timing (with Stopwatch). The topmost graph is the time spent in the Draw method and the bottom graph is the time from the end of Draw until the start of Update Draw and xna timing

The spikes are almost exactly 1 second apart and are always 2,3,4 or 5 times the usual time. The frames immediately following the spike take no time at all. I have checked that it is not the garbage collector.

I'm currently instancing a mesh consisting of 12 triangles and 36 vertices as a triangle list (I know it's not optimal, but it's just for testing) with 1 million instances. If I batch the instancing draw calls into small parts of 250 instances each the problem is alleviated, but cpu usage increases significantly. The run above is at 10000 instance per draw call, which is much easier on the cpu.

If I run the game in fullscreen the bottom graph is almost non-existent, but the same problem occurs now in the Draw method.

Here is a run inside PIX, which makes no sense to me at all. It seems for some frames there is no rendering done...

Any idea, what might be causing this?

EDIT: As requested, the relevant portions of the render code

A CubeBuffer is created and initialized, then filled with cubes. If the amount of cubes is above a certain limit, a new CubeBuffer is created, and so on. Each buffer draws all instances in one call.

Information needed only once is static (vertex, index buffer and vertex declaration; although it makes no difference so far). The texture is 512x512

Draw()

device.Clear(Color.DarkSlateGray);
device.RasterizerState = new RasterizerState() {  };
device.BlendState = new BlendState { };
device.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };

//samplerState=new SamplerState() { AddressU = TextureAddressMode.Mirror, AddressV = TextureAddressMode.Mirror, Filter = TextureFilter.Linear };
device.SamplerStates[0] = samplerState
effect.CurrentTechnique = effect.Techniques["InstancingTexColorLight"];
effect.Parameters["xView"].SetValue(cam.viewMatrix);
effect.Parameters["xProjection"].SetValue(projectionMatrix);
effect.Parameters["xWorld"].SetValue(worldMatrix);
effect.Parameters["cubeTexture"].SetValue(texAtlas);
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    pass.Apply();

foreach (var buf in CubeBuffers)
    buf.Draw();
base.Draw(gameTime);

CubeBuffer

[StructLayout(LayoutKind.Sequential)]
struct InstanceInfoOpt9
    {
    public Matrix World;
    public Vector2 Texture;
    public Vector4 Light;
    };

static VertexBuffer geometryBuffer = null;
static IndexBuffer geometryIndexBuffer = null;
static VertexDeclaration instanceVertexDeclaration = null;
VertexBuffer instanceBuffer = null;
InstanceInfoOpt9[] Buffer = new InstanceInfoOpt9[MaxCubeCount];
Int32 bufferCount=0

Init()
    {
    if (geometryBuffer == null)
        {
        geometryBuffer = new VertexBuffer(Device, typeof (VertexPositionTexture), 36, BufferUsage.WriteOnly);
        geometryIndexBuffer = new IndexBuffer(Device, typeof (Int32), 36, BufferUsage.WriteOnly);
        vertices = new[]{...}
        geometryBuffer.SetData(vertices);
        indices = new[]{...}
        geometryIndexBuffer.SetData(indices);

        var instanceStreamElements = new VertexElement[6];
        instanceStreamElements[0] = new VertexElement(sizeof (float)*0, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1);
        instanceStreamElements[1] = new VertexElement(sizeof (float)*4, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 2);
        instanceStreamElements[2] = new VertexElement(sizeof (float)*8, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 3);
        instanceStreamElements[3] = new VertexElement(sizeof (float)*12, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 4);
        instanceStreamElements[4] = new VertexElement(sizeof (float)*16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 5);
        instanceStreamElements[5] = new VertexElement(sizeof (float)*18, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 6);

        instanceVertexDeclaration = new VertexDeclaration(instanceStreamElements);
        }

    instanceBuffer = new VertexBuffer(Device, instanceVertexDeclaration, MaxCubeCount, BufferUsage.WriteOnly);
    instanceBuffer.SetData(Buffer);
    bindings = new[]
        {
        new VertexBufferBinding(geometryBuffer), 
        new VertexBufferBinding(instanceBuffer, 0, 1),
            };
    }

AddRandomCube(Vector3 pos)
    {
    if(cubes.Count >= MaxCubeCount)
        return null;
    Vector2 tex = new Vector2(rnd.Next(0, 16), rnd.Next(0, 16))
    Vector4 l= new Vector4((float)rnd.Next(), (float)rnd.Next(), (float)rnd.Next(), (float)rnd.Next());
    var cube = new InstanceInfoOpt9(Matrix.CreateTranslation(pos),tex, l);

    Buffer[bufferCount++] = cube;

    return cube;
    }

Draw()
    {
    Device.Indices = geometryIndexBuffer;
    Device.SetVertexBuffers(bindings);
    Device.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 36, 0, 12, bufferCount);
    }

Shader

float4x4 xView;
float4x4 xProjection;
float4x4 xWorld;
texture cubeTexture;

sampler TexColorLightSampler = sampler_state
{
texture = <cubeTexture>;
mipfilter = LINEAR;
minfilter = LINEAR;
magfilter = LINEAR;
};

struct InstancingVSTexColorLightInput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
};

struct InstancingVSTexColorLightOutput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float4 Light : TEXCOORD1;
};

InstancingVSTexColorLightOutput InstancingVSTexColorLight(InstancingVSTexColorLightInput input, float4x4 instanceTransform : TEXCOORD1, float2 instanceTex : TEXCOORD5, float4 instanceLight : TEXCOORD6)
{
float4x4 preViewProjection = mul (xView, xProjection);
float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);

InstancingVSTexColorLightOutput output;
float4 pos = input.Position;

pos = mul(pos, transpose(instanceTransform));
pos = mul(pos, preWorldViewProjection);

output.Position = pos;
output.Light = instanceLight;
output.TexCoord = float2((input.TexCoord.x / 16.0f) + (1.0f / 16.0f * instanceTex.x), 
                         (input.TexCoord.y / 16.0f) + (1.0f / 16.0f * instanceTex.y));

return output;
}

float4 InstancingPSTexColorLight(InstancingVSTexColorLightOutput input) : COLOR0
{
float4 color = tex2D(TexColorLightSampler, input.TexCoord);

color.r = color.r * input.Light.r;
color.g = color.g * input.Light.g;
color.b = color.b * input.Light.b;
color.a = color.a * input.Light.a;

return color;
}

technique InstancingTexColorLight
{
 pass Pass0
 {
 VertexShader = compile vs_3_0 InstancingVSTexColorLight();
 PixelShader = compile ps_3_0 InstancingPSTexColorLight();
 }
}
\$\endgroup\$
3
  • \$\begingroup\$ I'm not sure if it's relevant to time from end of draw to begining of update, since they are not strongly linked (i.e. many updates can happen between 2 draws if the game runs slowly, which must be the case since you are not running at 60 fps). They might even run in separate threads (but I'm not sure about this). \$\endgroup\$
    – Zonko
    Jul 29, 2012 at 21:20
  • \$\begingroup\$ I have no real clue atm, but if it works with smaller batching its apparently a problem with your batching code, post the relevant XNA and HLSL code so we can look more closely at it @Zonko with IsFixedTimeStep = False there is 1:1 update/draw calls \$\endgroup\$ Jul 29, 2012 at 22:42
  • \$\begingroup\$ Here is an explanation for why this stuttering happens from Shawn Hargreaves (on the xna team): forums.create.msdn.com/forums/p/9934/53561.aspx#53561 \$\endgroup\$
    – NexAddo
    Jul 30, 2012 at 17:56

2 Answers 2

3
\$\begingroup\$

I'm guessing that your performance is GPU-bound. You're simply asking your graphics device to do more work per unit time than it's capable of handling; 36 million vertices per frame is a pretty decent number, and hardware instancing can actually increase the amount of processing work necessary on the GPU side of the equation. Draw fewer polygons.

Why does reducing the batch size make the problem go away? Because it makes the CPU take longer to process a frame, which means it's spending less time sitting inside of Present() waiting for the GPU to finish rendering. That's what I think it's doing during that gap at the end of your Draw() calls.

The reason behind the specific timing of the gaps is harder to divine without understanding the whole of the code, but I'm not sure it's important, either. Do more work on the CPU, or less work on the GPU, so that your workload is less uneven.

See this article on Shawn Hargreaves' blog for more information.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ It is definitely GPU bound. The app is essentially a benchmark, to explore different drawing methods. A smaller batch size with the same amount of vertices drawn would take longer on the CPU, but GPU-load should be the same, no? At least I would expect a consistent time between frames, depending on load (which does not change between frames at all) and not such regular intervals of lag and instant(or no rendering, see PIX). \$\endgroup\$
    – Darcara
    Jul 30, 2012 at 16:04
  • \$\begingroup\$ If I'm interpreting your graphs correctly, the instantly-rendered frames are part of the functionality of the XNA Framework. With IsFixedTimeStep set to false, If the game is running too slowly, XNA will call Update() multiple times in a row to catch up, deliberately dropping frames in the process. Is IsRunningSlowly set to true during these frames? As for the strange timing - it does make me wonder a bit. Are you running in windowed mode? Does the behavior persist in fullscreen mode? \$\endgroup\$ Jul 30, 2012 at 20:21
  • \$\begingroup\$ catchup-calls happen only on IsFixedTimeStep=true. The bottom graph shows the time between the end of my draw and the beginning of the update call of the next frame. The frames are not dropped, I call the draw-methods and pay the CPU price for them (top graph). Same behavior in fullscreen and across resolutions. \$\endgroup\$
    – Darcara
    Jul 30, 2012 at 20:55
  • \$\begingroup\$ You're right, my mistake. I'm afraid I've exhausted my ideas at this point. \$\endgroup\$ Jul 30, 2012 at 20:56
2
\$\begingroup\$

I think you have a garbage problem... maybe you are creating/destroying to many objects and that spikes are the garbage collector routine working...

be sure to reuse all your memory structures... and don't use 'new' too often

\$\endgroup\$
2
  • \$\begingroup\$ Already checked that in ProcessExplorer and CLRProfiler, and the gc is running like once every 10 seconds and not nearly as long as 75ms. \$\endgroup\$
    – Darcara
    Jul 29, 2012 at 7:53
  • 1
    \$\begingroup\$ You definitely don't want to create a new RasterizerState, BlendState and DepthStencilState every frame regardless of whether or not it is the cause of your rendering slowdown. It will definitely not help, as per this article blogs.msdn.com/b/shawnhar/archive/2010/04/02/… You should create the state you will be using once on load and reapply them when needed. \$\endgroup\$ Jul 30, 2012 at 15:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .