XNA stuttering at regular intervals

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

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 };

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),
};
}

{
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);
}


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
{
}
}

• 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). Jul 29 '12 at 21:20
• 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 Jul 29 '12 at 22:42
• 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 Jul 30 '12 at 17:56

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.

• 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? Jul 30 '12 at 20:21
• 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. Jul 30 '12 at 20:55