Original Post
Hey there,
I repeatedly here people complaining about how sloooow immediate mode is but, using it myself, I always thought "Can't be too bad for 2D, worked good so far". Due to some recent performance issues (The first in ~three years) I wrote a small benchmark to test excactly how slow immediate mode is.
I didn't believe it: Even in the worst case I could think of using VBOs, they are still 4 times faster than immediate mode?! Please, anyone, take a look at my code and tell me if it looks good. I never used VBOs before and don't want to rework my projects whole gfx engine and see that I did something terribly wrong during profiling.
The benchmark app is written in C# using the Tao framework. I just used a fixed vertex format assuming per-vertex 2d coords, three sets of 2d texcoords and a 4-float-alpha value.
Test cases are:
Immediate mode: ~ 10 ms
Immediate mode "Singe Surface" (single glBegin / glEnd): ~ 9,5 ms
Static VBO: ~1,2 ms
Streamed VBO used like immediate (store 1 quad, draw 1 quad, n times each frame): ~1,2 ms
Streamed VBO (Store once / Draw once each frame): ~0,8 ms
Streamed VBO double-buffered (Store to A / Draw B / switch both each frame): ~0,8 ms
I repeatedly here people complaining about how sloooow immediate mode is but, using it myself, I always thought "Can't be too bad for 2D, worked good so far". Due to some recent performance issues (The first in ~three years) I wrote a small benchmark to test excactly how slow immediate mode is.
I didn't believe it: Even in the worst case I could think of using VBOs, they are still 4 times faster than immediate mode?! Please, anyone, take a look at my code and tell me if it looks good. I never used VBOs before and don't want to rework my projects whole gfx engine and see that I did something terribly wrong during profiling.
The benchmark app is written in C# using the Tao framework. I just used a fixed vertex format assuming per-vertex 2d coords, three sets of 2d texcoords and a 4-float-alpha value.
Test cases are:
Immediate mode: ~ 10 ms
Immediate mode "Singe Surface" (single glBegin / glEnd): ~ 9,5 ms
Static VBO: ~1,2 ms
Streamed VBO used like immediate (store 1 quad, draw 1 quad, n times each frame): ~1,2 ms
Streamed VBO (Store once / Draw once each frame): ~0,8 ms
Streamed VBO double-buffered (Store to A / Draw B / switch both each frame): ~0,8 ms
using System;using System.Collections.Generic;using System.Text;using System.Runtime.InteropServices;using Tao.Glfw;using Tao.OpenGl;namespace OpenGL_Test{ public class Program { public const int NumPrimitives = 1000; public const int Iterations = 120; public static void Main(string[] args) { Glfw.glfwInit(); Glfw.glfwOpenWindow(640, 480, 8, 8, 8, 8, 32, 0, Glfw.GLFW_WINDOW); Glfw.glfwSwapInterval(0); // Vsync off Gl.glOrtho(0, 640, 480, 0, 0, 1); DoTest(new Immedate()); DoTest(new ImmedateSS()); DoTest(new VBOStatic()); DoTest(new VBOStreamCh()); DoTest(new VBOStream()); DoTest(new VBOStreamDB()); Console.ReadLine(); Glfw.glfwTerminate(); } public static void DoTest(ITest test) { test.Init(NumPrimitives); double frameTimeSum = 0.0d; int frameNum = 0; double currentTime; double frameTime; for (int i = 0; i < Iterations; i++) { currentTime = Glfw.glfwGetTime(); Gl.glClear(Gl.GL_COLOR | Gl.GL_DEPTH); test.Draw(NumPrimitives); Gl.glFinish(); Glfw.glfwSwapBuffers(); Glfw.glfwPollEvents(); frameTime = Glfw.glfwGetTime() - currentTime; frameTimeSum += frameTime; frameNum++; } Console.WriteLine( "{0}:\t{1:F} us", test.GetType().Name, 1000000.0d * frameTimeSum / frameNum); } } public interface ITest { void Draw(int n); void Init(int n); } [StructLayout(LayoutKind.Sequential)] public struct Vertex { public static readonly IntPtr VertexOffsetPtr = (IntPtr)(sizeof(float) * 0); public static readonly IntPtr Tex0OffsetPtr = (IntPtr)(sizeof(float) * 2); public static readonly IntPtr Tex1OffsetPtr = (IntPtr)(sizeof(float) * 4); public static readonly IntPtr Tex2OffsetPtr = (IntPtr)(sizeof(float) * 6); public static readonly IntPtr ColorOffsetPtr = (IntPtr)(sizeof(float) * 8); public static readonly int Size = (sizeof(float) * 16); public float x, y; public float s0, t0; public float s1, t1; public float s2, t2; public float r, g, b, a; public float pad0, pad1, pad2, pad3; public Vertex(float x, float y, float r, float g, float b, float a, float s0, float t0, float s1, float t1, float s2, float t2) { this.x = x; this.y = y; this.s0 = s0; this.t0 = t0; this.s1 = s1; this.t1 = t1; this.s2 = s2; this.t2 = t2; this.r = r; this.g = g; this.b = b; this.a = a; this.pad0 = 0; this.pad1 = 0; this.pad2 = 0; this.pad3 = 0; } public Vertex(float x, float y, float r, float g, float b, float a, float s0, float t0, float s1, float t1) : this(x, y, r, g, b, a, s0, t0, s1, t1, 0.0f, 0.0f) {} public Vertex(float x, float y, float r, float g, float b, float a, float s0, float t0) : this(x, y, r, g, b, a, s0, t0, 0.0f, 0.0f, 0.0f, 0.0f) {} public Vertex(float x, float y, float r, float g, float b, float a) : this(x, y, r, g, b, a, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) {} public Vertex(float x, float y) : this(x, y, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) {} } public class Immedate : ITest { public void Draw(int n) { for (int i = 0; i < n; i++) { Gl.glBegin(Gl.GL_QUADS); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(100.0f, 100.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(200.0f, 100.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(200.0f, 200.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(100.0f, 200.0f); Gl.glEnd(); } } public void Init(int n) { } } public class ImmedateSS : ITest { public void Draw(int n) { Gl.glBegin(Gl.GL_QUADS); for (int i = 0; i < n; i++) { Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(100.0f, 100.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(200.0f, 100.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(200.0f, 200.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE0, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE1, 0.0f, 0.0f); Gl.glMultiTexCoord2f(Gl.GL_TEXTURE2, 0.0f, 0.0f); Gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); Gl.glVertex2f(100.0f, 200.0f); } Gl.glEnd(); } public void Init(int n) { } } public class VBOStatic : ITest { protected int glIdVBO = 0; public void Draw(int n) { Gl.glBindBufferARB(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glEnableClientState(Gl.GL_VERTEX_ARRAY); Gl.glVertexPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.VertexOffsetPtr); Gl.glEnableClientState(Gl.GL_COLOR_ARRAY); Gl.glColorPointer(4, Gl.GL_FLOAT, Vertex.Size, Vertex.ColorOffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex0OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex1OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex2OffsetPtr); Gl.glDrawArrays(Gl.GL_QUADS, 0, 4 * n); Gl.glDisableClientState(Gl.GL_VERTEX_ARRAY); Gl.glDisableClientState(Gl.GL_COLOR_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glBindBufferARB(Gl.GL_ARRAY_BUFFER, 0); } public void Init(int n) { Random rnd = new Random(); Vertex[] vertices = new Vertex[4 * n]; for (int i = 0; i < n; i++) { vertices[i * 4 + 0] = new Vertex(100.0f, 100.0f); vertices[i * 4 + 1] = new Vertex(200.0f, 100.0f); vertices[i * 4 + 2] = new Vertex(200.0f, 200.0f); vertices[i * 4 + 3] = new Vertex(100.0f, 200.0f); } Gl.glGenBuffers(1, out glIdVBO); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(vertices.Length * Vertex.Size), vertices, Gl.GL_STATIC_DRAW); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); } } public class VBOStream : ITest { protected int glIdVBO = 0; public void Draw(int n) { Vertex[] vertices = new Vertex[4 * n]; for (int i = 0; i < n; i++) { vertices[i * 4 + 0] = new Vertex(100.0f, 100.0f); vertices[i * 4 + 1] = new Vertex(200.0f, 100.0f); vertices[i * 4 + 2] = new Vertex(200.0f, 200.0f); vertices[i * 4 + 3] = new Vertex(100.0f, 200.0f); } Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBufferSubData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), (IntPtr)(vertices.Length * Vertex.Size), vertices); Gl.glEnableClientState(Gl.GL_VERTEX_ARRAY); Gl.glVertexPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.VertexOffsetPtr); Gl.glEnableClientState(Gl.GL_COLOR_ARRAY); Gl.glColorPointer(4, Gl.GL_FLOAT, Vertex.Size, Vertex.ColorOffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex0OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex1OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex2OffsetPtr); Gl.glDrawArrays(Gl.GL_QUADS, 0, 4 * n); Gl.glDisableClientState(Gl.GL_VERTEX_ARRAY); Gl.glDisableClientState(Gl.GL_COLOR_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glBindBufferARB(Gl.GL_ARRAY_BUFFER, 0); } public void Init(int n) { Gl.glGenBuffers(1, out glIdVBO); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); } } public class VBOStreamDB : ITest { protected int glIdVBO1 = 0; protected int glIdVBO2 = 0; public void Draw(int n) { Vertex[] vertices = new Vertex[4 * n]; for (int i = 0; i < n; i++) { vertices[i * 4 + 0] = new Vertex(100.0f, 100.0f); vertices[i * 4 + 1] = new Vertex(200.0f, 100.0f); vertices[i * 4 + 2] = new Vertex(200.0f, 200.0f); vertices[i * 4 + 3] = new Vertex(100.0f, 200.0f); } Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO1); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBufferSubData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), (IntPtr)(vertices.Length * Vertex.Size), vertices); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO2); Gl.glEnableClientState(Gl.GL_VERTEX_ARRAY); Gl.glVertexPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.VertexOffsetPtr); Gl.glEnableClientState(Gl.GL_COLOR_ARRAY); Gl.glColorPointer(4, Gl.GL_FLOAT, Vertex.Size, Vertex.ColorOffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex0OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex1OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex2OffsetPtr); Gl.glDrawArrays(Gl.GL_QUADS, 0, 4 * n); Gl.glDisableClientState(Gl.GL_VERTEX_ARRAY); Gl.glDisableClientState(Gl.GL_COLOR_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); int swap = glIdVBO1; glIdVBO1 = glIdVBO2; glIdVBO2 = swap; } public void Init(int n) { Gl.glGenBuffers(1, out glIdVBO1); Gl.glGenBuffers(1, out glIdVBO2); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO1); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO2); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); } } public class VBOStreamCh : ITest { protected int glIdVBO = 0; public void Draw(int n) { Vertex[] vertices = new Vertex[4]; vertices[0] = new Vertex(100.0f, 100.0f); vertices[1] = new Vertex(200.0f, 100.0f); vertices[2] = new Vertex(200.0f, 200.0f); vertices[3] = new Vertex(100.0f, 200.0f); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glEnableClientState(Gl.GL_VERTEX_ARRAY); Gl.glVertexPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.VertexOffsetPtr); Gl.glEnableClientState(Gl.GL_COLOR_ARRAY); Gl.glColorPointer(4, Gl.GL_FLOAT, Vertex.Size, Vertex.ColorOffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex0OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex1OffsetPtr); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glEnableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glTexCoordPointer(2, Gl.GL_FLOAT, Vertex.Size, Vertex.Tex2OffsetPtr); for (int i = 0; i < n; i++) { Gl.glBufferSubData(Gl.GL_ARRAY_BUFFER, (IntPtr)(i * 4 * Vertex.Size), (IntPtr)(4 * Vertex.Size), vertices); Gl.glDrawArrays(Gl.GL_QUADS, 4 * i, 4); } Gl.glDisableClientState(Gl.GL_VERTEX_ARRAY); Gl.glDisableClientState(Gl.GL_COLOR_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE0); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE1); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glClientActiveTextureARB(Gl.GL_TEXTURE2); Gl.glDisableClientState(Gl.GL_TEXTURE_COORD_ARRAY); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); } public void Init(int n) { Gl.glGenBuffers(1, out glIdVBO); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, glIdVBO); Gl.glBufferData(Gl.GL_ARRAY_BUFFER, (IntPtr)(0), null, Gl.GL_STREAM_DRAW); Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); } }}