Faster images drawing?

Hello! I’m new to OpenGL programming and graphics in general. Recently I faced with some problem: I wanted to draw images (as a fullscreen tilemap). Firstly I used System.Drawing (as C# is the only language I know decently), but drawing 32x32 tiles on 800x600 display resulted extremly low fps (1-2), so I switched to OpenTK library. As I said, im only a beginner, so my code (based on tutorials and first chapters of books) is obviously not effective.

Drawing events: http://pastebin.com/rduEMv2L

	// GLControl Events
	protected override void OnGLControlLoad(object sender, EventArgs e)
        {
            base.OnGLControlLoad(sender, e);

            _sp = ShaderProgram.LoadShader("default");
            _sp.Use();

            _sprite = new Sprite("tile");
            _sprite.Initialize();
        }

        protected override void OnGLControlPaint(object sender, EventArgs e)
        {
            //GL.ClearColor(Color.Black);
            GL.Clear(ClearBufferMask.ColorBufferBit);

            for (int y = 0; y < glControl.Height; y += _sprite.Height)
            {
                for (int x = 0; x < glControl.Width; x += _sprite.Width)
                {
                    _sprite.Render(glControl, x, y);
                }
            }

            glControl.SwapBuffers();
        }
		
		// Sprite#Initialize and Sprite#Render methods
		public void Initialize()
        {
            GraphicsEngine.Imaging.InitializeTexture(_bmp, out _textureId);
            Initialized = true;
        }
 		
        public void Render(GLControl control, float x, float y)
        {
            if (Initialized)
            {
                GraphicsEngine.Imaging.DrawImage(control, x, y, Size.Width, Size.Height, _textureId, _color, out _vao);
            }
        }

		// GraphicsEngine#InitializeTexture
 		public static void InitializeTexture(Bitmap bmp, out int id)
            {
                GL.GenTextures(1, out id);
                GL.BindTexture(TextureTarget.Texture2D, id);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
                BitmapData data = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, GLPixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
                bmp.UnlockBits(data);
                bmp.Dispose();
            }

		// GraphicsEngine.Imaging#DrawImage
		public static void DrawImage(GLControl control, float x, float y, float width, float height, int textureId, out VertexArray vao)
        {
            DrawImage(control, x, y, width, height, textureId, Utilities.WhiteColorArray4, out vao);
        }

		public static void DrawImage(GLControl control, float x, float y, float width, float height, int textureId, float[] colors, out VertexArray vao)
        {
            GL.BindTexture(TextureTarget.Texture2D, textureId);
            Primitives.DrawRectangle(control, x, y, width, height, colors, true, out vao);
        }

		// GraphicsEngine.Primitives#DrawRectangle
		public static void DrawRectangle(GLControl control, float x, float y, float width, float height, float[] colors, bool textured, out VertexArray vao)
        {
                // Transforms points (in pixels) to OpenGL coordinates system
                // Left bottom point (-1; -1)
                float x1 = -1 + (2 * x / control.Width);
                float y1 = -1 + (2 * y / control.Height);
                // Left upper point (in pixels) to (-1; 1)
                float x2 = x1;
                float y2 = y1 + (2 * height / control.Height);
                // Right upper point (in pixels) to (1; 1)
                float x3 = x1 + (2 * width / control.Width);
                float y3 = y2;
                // Right bottom point (in pixels) to (1; -1)
                float x4 = x3;
                float y4 = y1;
               
                var vertices = new float[] { x1, y1, x2, y2, x3, y3, x4, y4 };
                vao = CreateVertexArray(2);
                vao.PutVertices(vertices);
                vao.PutData(colors, 4);
                if (textured)
                {
                    vao.PutData(DefaultTexCoords, 2);
                }
                vao.PutIndices(VertexIndices4);
                vao.Initialize();
                vao.Render(BeginMode.Quads);
        }
    }

VertexArray class: http://pastebin.com/0ywnsHm8

using System;
using System.Collections.Generic;

using OpenTK.Graphics.OpenGL;

namespace SGE
{
    public sealed class VertexArray : IDisposable
    {
        private int _dimensions;

        private int _ibo;
        private int _vbo;
        private int _vao;

        private int _indexCount;

        private readonly List<VertexData> _data;

        private bool _disposed;

        public bool Initialized { get; private set; }

        internal VertexArray(int dimensions)
        {
            _dimensions = dimensions;
            _data = new List<VertexData>();
        }

        ~VertexArray()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            if (disposing && Initialized)
            {
                GL.DeleteBuffer(_ibo);
                GL.DeleteVertexArray(_vao);
                Initialized = false;
            }

            _disposed = true;
            GC.SuppressFinalize(this);
        }

        public bool PutIndices(int[] indicies)
        {
            if (indicies.Length < 3)
                return false;

            _ibo = GL.GenBuffer();
            if (_ibo == 0)
                return false;

            GL.BindBuffer(BufferTarget.ArrayBuffer, _ibo);
            GL.BufferData(BufferTarget.ArrayBuffer, indicies.Length * sizeof(uint), indicies, BufferUsageHint.StaticDraw);

            _indexCount = indicies.Length;

            return true;
        }

        public bool PutVertices(float[] vertices)
        {
            if (_vbo != 0 || vertices.Length < 6)
                return false;

            _vbo = GL.GenBuffer();
            if (_vbo == 0)
                return false;

            GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
            GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);

            return true;
        }

        public bool PutData(float[] data, int size)
        {
            int dbo = GL.GenBuffer();
            if (dbo == 0)
                return false;

            GL.BindBuffer(BufferTarget.ArrayBuffer, dbo);
            GL.BufferData(BufferTarget.ArrayBuffer, data.Length * sizeof(float), data, BufferUsageHint.StaticDraw);
            _data.Add(new VertexData(dbo, size));

            return true;
        }

        public bool Initialize()
        {
            _vao = GL.GenVertexArray();
            if (_vao == 0)
                return false;

            GL.BindVertexArray(_vao);
            GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
            GL.VertexAttribPointer(0, _dimensions, VertexAttribPointerType.Float, false, 0, 0);
            GL.EnableVertexAttribArray(0);

            for (int i = 0; i < _data.Count; i++)
            {
                GL.BindBuffer(BufferTarget.ArrayBuffer, _data[i].Buffer);
                GL.VertexAttribPointer(i + 1, _data[i].Size, VertexAttribPointerType.Float, false, 0, 0);
                GL.EnableVertexAttribArray(i + 1);
            }
            _data.Clear();

            Initialized = true;

            return true;
        }

        public void Render(BeginMode beginMode)
        {
            if (!Initialized)
                return;

            GL.BindVertexArray(_vao);
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, _ibo);

            GL.DrawElements(beginMode, _indexCount, DrawElementsType.UnsignedInt, 0);

            GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0);
            GL.BindVertexArray(0);
        }

        private struct VertexData
        {
            public int Buffer { get; private set; }
            public int Size { get; private set; }

            public VertexData(int buffer, int size)
            {
                Buffer = buffer;
                Size = size;
            }
        }
    }
}

I also used default vertex (http://pastebin.com/KEZ85GDb) and fragment (http://pastebin.com/7cnfFfb0) shaders:

Vertex shader:

#version 330 core

layout (location = 0) in vec4 position;
layout (location = 1) in vec4 color;
layout (location = 2) in vec2 texCoord;

out vec4 fColor;
out vec2 fTexCoord;

void main()
{
	gl_Position = position;
	fColor = color;
	fTexCoord = texCoord;
}

Fragment shader:

#version 330 core

in vec4 fColor;
in vec2 fTexCoord;

out vec4 color;

uniform sampler2D _texture;

void main()
{
	color = texture(_texture, fTexCoord) * fColor;
}

I get 40-60 fps in 800x600 resolution and 7-11 in FullHD.
How can I improve the drawing process?
Thanks in advance!

P.S. I can’t post links, so I removed dots and slashes. I hope, it’s not prohibited, I just want to save space in the topic.

By not using a separate VAO and draw call for each rectangle.

And what can I do instead?
P.S. What’s the point of the 30 characters limitation? I can’t even reply without this postscriptum : /

Put all of the rectangles for the current frame (or even the entire map, if it isn’t huge) into a single VAO and draw all of the visible rectangles with a single glDrawElements call.

There are techniques to optimise it further, but for such a small amount of data there really isn’t any need.

Thanks! That really helped.

How can I do this? I tried to pass the color data for each tile and it has hit the performance.

It depends entirely upon the specifics of the usage case.

For the case of a static tilemap where the only thing changing each frame is the scroll offset, you can upload all of the per-vertex data (position, texture coordinates) during initialisation, and can also create a static element array for a screen-sized portion of it. The map can be scrolled simply by changing the base vertex in a call to glDrawElementsBaseVertex and a uniform offset which is added to the vertex position in the vertex shader.

If you need to change aspects of the map between frames, then care needs to be taken to avoid synchronisation (where the CPU has to wait until the GPU has rendered any previous frames which use the data). See the Buffer Object Streaming page on the wiki for information on efficiently updating buffers.

If the map is too large to simply store the entire map in GPU memory (and unless you’re dealing with a mobile platform, that would have to be a very large map), then a 2D circular buffer or a “chunked” map allows you to scroll the map without having to re-upload everything when a new row/column of tiles become visible.