How do you split indices and vertices if their quantity goes over the limit?

I want to draw a flat surface of 500x500 squares which are built out of four vertices by two triangles.

There are 1,000,000 vertices and 1,500,000 indices.

The Reach profile of xna, limits primitives to 65535 per draw call.

My thought was to split whole data twice, first for indices using the same baseVertex, and second to stay beyond 65535 (ushort.MaxValue).

static class Painter
{
public struct IndexedPrimitives
{
public PrimitiveType Type;
public int baseVertex;
public int minVertexIndex;
public int numVertices;
public int startIndex;
public int primitiveCount;
}
public struct IndexedPrimitivesList
{
public VertexBuffer vb;
public IndexBuffer ib;
public IndexedPrimitives[] primitives;
}
public static IndexedPrimitivesList GenerateIndexedTirangleList(GraphicsDevice GraphicsDevice, VertexBuffer vb, int[] indices)
{
IndexedPrimitivesList ret;
List<int> breaks = new List<int>();
List<int> levels = new List<int>();
ushort[] inds = new ushort[indices.Length];

for (int i = 0; i < indices.Length; i++)
{
if (indices[i] / (ushort.MaxValue + 1) != levels[levels.Count - 1])
{
}
inds[i] = (ushort)indices[i];
}
indices = null;
ret.ib = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, inds.Length, BufferUsage.None);
ret.ib.SetData<ushort>(inds);
inds = null;
for (int i = 1; i < breaks.Count; i++)
{
if ((breaks[i] - breaks[i - 1]) % 3 != 0) throw new Exception("Triangles are using to far away vertices, or bad placed indices, foreach index of a triangle it needs index/65536 to be equal");
}
List<IndexedPrimitives> prims = new List<IndexedPrimitives>();

for (int i = 1; i < breaks.Count; i++)
{
int r = breaks[i] - breaks[i - 1];
while ((r / 3) / (ushort.MaxValue + 1) > 0)
{
IndexedPrimitives ip = new IndexedPrimitives();
ip.Type = PrimitiveType.TriangleList;
ip.baseVertex = levels[i - 1] * (ushort.MaxValue + 1);
ip.numVertices = ushort.MaxValue + 1;
ip.startIndex = breaks[i] - r;
ip.primitiveCount = ushort.MaxValue;
r -= (ushort.MaxValue + 1) * 3;
}
if (r != 0)
{
IndexedPrimitives ip = new IndexedPrimitives();
ip.Type = PrimitiveType.TriangleList;
ip.baseVertex = levels[i - 1] * (ushort.MaxValue + 1);
ip.numVertices = ushort.MaxValue + 1;
ip.startIndex = breaks[i] - r;
ip.primitiveCount = r / 3;
r -= r;
}
}
int rr = ret.ib.IndexCount - breaks[breaks.Count - 1];
while ((rr / 3) / (ushort.MaxValue + 1) > 0)
{
IndexedPrimitives ip = new IndexedPrimitives();
ip.Type = PrimitiveType.TriangleList;
ip.baseVertex = levels[levels.Count - 1] * (ushort.MaxValue + 1);
ip.numVertices = ushort.MaxValue + 1;
ip.startIndex = ret.ib.IndexCount - rr;
ip.primitiveCount = ushort.MaxValue;
rr -= (ushort.MaxValue + 1) * 3;
}
if (rr != 0)
{
IndexedPrimitives ip = new IndexedPrimitives();
ip.Type = PrimitiveType.TriangleList;
ip.baseVertex = levels[levels.Count - 1] * (ushort.MaxValue + 1);
ip.numVertices = vb.VertexCount-ip.baseVertex-1;
ip.startIndex = ret.ib.IndexCount - rr;
ip.primitiveCount = rr / 3;
rr -= rr;
}

ret.vb = vb;
ret.primitives = prims.ToArray();
return ret;
}
public static void DrawIndexedPrimitivesList(GraphicsDevice GraphicsDevice, IndexedPrimitivesList ipl)
{
GraphicsDevice.SetVertexBuffer(ipl.vb);
GraphicsDevice.Indices = ipl.ib;
foreach (IndexedPrimitives ip in ipl.primitives)
{
GraphicsDevice.DrawIndexedPrimitives(ip.Type, ip.baseVertex, ip.minVertexIndex, ip.numVertices, ip.startIndex, ip.primitiveCount);
}
}
public struct SplittedBuffers
{
public IndexBuffer[] ibs;
public VertexBuffer[] vbs;
}
public static SplittedBuffers GenerateSplittedBuffers(IndexedPrimitivesList ipl)
{
SplittedBuffers sb=new SplittedBuffers();
List<List<byte>> vbs = new List<List<byte>>();
List<List<byte>> ibs = new List<List<byte>>();
List<int> baseVertices = new List<int>();
byte[] vb = new byte[ipl.vb.VertexCount * ipl.vb.VertexDeclaration.VertexStride];
ipl.vb.GetData<byte>(vb);
byte[] ib = new byte[ipl.ib.IndexCount * ((ipl.ib.IndexElementSize == IndexElementSize.SixteenBits) ? 2 : 4)];
ipl.ib.GetData<byte>(ib);
foreach (IndexedPrimitives ip in ipl.primitives)
{
int i = baseVertices.IndexOf(ip.baseVertex);
if (i < 0)
{
i = vbs.Count-1;
}
byte[] tmp = new byte[ip.numVertices * ipl.vb.VertexDeclaration.VertexStride];
Array.Copy(vb, ip.baseVertex * ipl.vb.VertexDeclaration.VertexStride, tmp, 0, tmp.Length);

tmp = new byte[ip.primitiveCount*((ip.Type==PrimitiveType.TriangleList)?3:1) * ((ipl.ib.IndexElementSize == IndexElementSize.SixteenBits) ? 2 : 4)];
Array.Copy(ib, ip.startIndex * ((ipl.ib.IndexElementSize == IndexElementSize.SixteenBits) ? 2 : 4), tmp, 0, tmp.Length);

}
sb.ibs = new IndexBuffer[ibs.Count];
sb.vbs = new VertexBuffer[vbs.Count];
for (int i = 0; i < vbs.Count; i++)
{
sb.ibs[i] = new IndexBuffer(ipl.ib.GraphicsDevice, ipl.ib.IndexElementSize, ibs[i].Count / ((ipl.ib.IndexElementSize == IndexElementSize.SixteenBits) ? 2 : 4), ipl.ib.BufferUsage);
sb.ibs[i].SetData<byte>(ibs[i].ToArray());
sb.vbs[i] = new VertexBuffer(ipl.vb.GraphicsDevice, ipl.vb.VertexDeclaration, vbs[i].Count / ipl.vb.VertexDeclaration.VertexStride, ipl.vb.BufferUsage);
sb.vbs[i].SetData<byte>(vbs[i].ToArray());
}
return sb;
}
public static void DrawSplittedBuffers(GraphicsDevice GraphicsDevice,SplittedBuffers sb)
{
for (int i = 0; i < sb.vbs.Length; i++)
{
GraphicsDevice.SetVertexBuffer(sb.vbs[i]);
GraphicsDevice.Indices = sb.ibs[i];
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, sb.vbs[i].VertexCount, 0, sb.ibs[i].IndexCount/3);
}
}
}


Edits:

I thought up to now vertices and indices are limited to 65535, but the primitives are.

There's other strange behavior of the parameter numVertices, it can be set to a bigger Value then the actual number of vertices in the buffer without throwing an exception or aborting.

The code above now works for me. But it is a bit too slow.

Further edits:

I improved my code to prepare all the values only once. During draw call only the values are inserted, now I'd like to redefine my Question as follow: Is switching Buffers faster then the drawing method above?

I suppose it's not, because the same number of draw calls are needed by both methods, as both methods can't draw in one call triangles with different baseVertex or more then 65535 primitives. May switching the buffers will also slow down a little bit and I think using the offsets baseVertext and startIndex should not slow down the drawing method. What are your thoughts and experiences?

Tested

I tested both drawing methods. For the same data both need the same number of draw calls (in my tests 22 per draw). Drawing the SplittedBuffers is up 3 times slower then using the IndexedPrimitivesList. I will now do some changes in the generation to allow overlapping vertex ranges.

• 1M vertices and 1.5M indices is a huge number. If you are targeting on Xbox or WP7, forget it. If you are targeting on PC, just use HiDef profile. And even on PC that is a huge number of vertices and will require high-end(ish) computer. – user9790 Feb 6 '12 at 20:51
• It's only for testing right now, I store the vertices's byte data in a File (uses 23MB) and its quite fast loaded. Each vertex contains only Position and Color information. – user0817 Feb 6 '12 at 20:56
• @Jaakko Lipsanen: Indeed you showed me the next limit, 67108863 for VertexBuffers. But I think it should be possible to draw many more vertices. I got over 6'000'000 indices and 4'000'000 vertices loaded very fast, on my low end laptop. Well right now they are not correctly drawn to screen. But why do you throw Xbox and Windows Phone into the same drawer? Isn't between them the biggest difference? – user0817 Feb 6 '12 at 22:49
• You need to separate your draw calls. It will require different vertex buffers with probably some duplication of vertices. Divide and conquer. – deceleratedcaviar Feb 6 '12 at 23:19
• Like @Daniel says, exactly. Each block will be a separate draw call involving a whole new set of uploaded verts+indices, and at the edges you will need a row of duplicated data that matches the edge of the block next to it. – Patrick Hughes Feb 7 '12 at 0:16

In DirectX there is a method:
ID3D11DeviceContext::DrawIndexed(UINT IndexCount,UINT StartIndexLocation, INT BaseVertexLocation);

Which allows you to specify where in the buffer to start drawing and how many to draw. If there is anything similar in XNA, wouldn't it be as simple as splitting it into several draw calls, without having to split the buffer?

void draw_big_mesh(ID3D11DeviceContext *devcon, ID3D11Buffer *vertexBuffer, int numIndices)
{
int limit = 65535;
int numPrimitives = numIndices/3; //assuming your primitives are triangles
int numSegments = numPrimitives / limit;
int rest = numPrimitives % limit;

for (int offset=0; offset < numSegments;offset++)
{
devcon->DrawIndexed(limit*3, offset*limit*3, vertexBuffer);
}

//draw rest
if (!rest)
devcon->DrawIndexed(rest*3, numSegments*3, vertexBuffer);
}

• Your example can't be used with more then 65535 vertices, because there is no baseVertex used which points the overflown indices to the right vertices. – user0817 Feb 8 '12 at 14:54
• I thought you said only the primitives per draw call were limited to 65535, not the vertices in a buffer. You said in your comment with the higher profile up to ~67 million verts in a buffer are possible? – user13213 Feb 8 '12 at 18:03
• But indices are ushort, so they can't point to vertices higher then 65535 without a offset called baseVertex. – user0817 Feb 8 '12 at 18:22
• @user0817 You can use IndexElementSize.ThirtyTwoBits to have 32-bit indices. That is available on HiDef profile. – user9790 Feb 12 '12 at 2:08
• I try to avoid HiDef. – user0817 Feb 12 '12 at 3:59