Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / X11

Copy a GL Texture to Another GL Texture or to a GL Pixel Buffer and from a GL Pixel Buffer

5.00/5 (2 votes)
13 Oct 2018CPOL3 min read 10.5K  
How to use GL's pixel buffer object (PBO) to copy one texture to another

Introduction

I wanted to rework my FreeType based text renderers "FreeTypeGlyphWise" und "FreeTypeLineWise" for OpenGL/OpenTK (on Linux using Mesa libraries). The goal was not to allocate the texture, that caches the glyphs to render at once for all possible glyphs of a font face and font size, but to allocate the texture with an initial set of glyphs and increase the texture size if needed (in case of more glyphs are to cache).

I think the requirement to preserve and resize a texture is a general one - but I didn't find any ready-to-use article or forum answer across the web. That's why I want to share the knowledge I gathered.

Background

OpenGL textures are situated within the driver/GPU controlled memory. To preserve and resize a texture, a technology should be used, that works entirely within the driver/GPU controlled memory (in the hope to avoid data transfer between driver/GPU controlled memory and main/CPU controlled memory) to achieve a good performance.

Before you go on reading, I want to state that I am not an OpenGL/OpenTK professional. The solution I want to describe here works very well for my purpose and has the potential to be a generic fallback for OpenGL older than 3.1.

As far as I know (after my web search) there are several ways to achieve this:

  • Texture copy: For OpenGL 4.3ff or older versions with ARB_copy_image extension (or with the older NV_copy_image extension) supported hardware + driver, you can use glCopyImageSubData().
  • Framebuffer copy 1/FBO blitting: For OpenGL 3.1ff or older versions with ARB_framebuffer_object extension supported hardware + driver, you can attach the source texture to an FBO, attach the destination texture to an FBO, set the read buffers and draw buffers for the FBOs to read from the source attachment and draw to the destination attachment, and then use glBlitFramebuffer() to blit from one framebuffer to the other.
  • Framebuffer copy 2/FBO copying: Furthermore, there are several other ways to copy the source texture via frame buffer object (FBO) to the target texture. Typically, you need OpenGL 3.1ff or older versions with ARB_framebuffer_object extension supported hardware + driver. Read this forum thread for an overview and performance indication on these alternatives.
  • Pixelbuffer copy: This is what I want to introduce (because pixelbuffer is guaranteed to be part of OpenGL 2.1ff) ...
  • Bitmap copy: Download the source texture from driver/GPU controlled memory to main/CPU controlled memory and back to driver/GPU controlled memory. This is what I want to avoid!

My environment is MonoDevelop 5.10 (older versions are possible as well) on openSuse Leap 42.1 64 bit with Mesa libraries. My Linux runs on VMWare Player. The version string, I get from GL.GetString(StringName.Version) for this environment, is "2.1 Mesa 10.3.7".

That's why I focus on a solution, that works guaranteed for OpenGL 2.1ff.

Using the Code

I use a wrapper class FtTexture to manage GL texture including the texture metadata. This class provides the CreateAndBindPhysicalTexture() method.

C#
/// <summary>
/// Creates and binds a new physical texture based on the actual pixel format and physical size.
/// </summary>
/// <param name="data">Pointer to the raw data to use. Can be <c>System.IntPtr.Zero</c>.</param>
/// <returns>The GL texture identifier.</returns>
public int CreateAndBindPhysicalTexture(System.IntPtr data)
{
#if DEBUG
    string methodName = "CreateAndBindPhysicalTexture";
#endif
            
    // Make sure that extensions are initialized (edge_clamp and sRGB extensions doesn't need new
    // function pointers, but call it anyway).
    GlUtil.VersionInfo.Current.EnsureExtensionsInit();

    // Check whether one of the extensions "GL_SGIS_texture_edge_clamp" or
    // "GL_EXT_texture_edge_clamp" is supported.
    // This will prevent the clamp algorithm from using the border texel/border color for sampling.
    textureEdgeClamp = GlUtil.VersionInfo.Current.GLEXT_SGIS_texture_edge_clamp ||
                       GlUtil.VersionInfo.Current.GLEXT_EXT_texture_edge_clamp;
    int  clampToEdge = (GlUtil.VersionInfo.Current.GLEXT_SGIS_texture_edge_clamp ?
                        (int)GlUtil.Globals.GL_CLAMP_TO_EDGE_SGIS :
                        (GlUtil.VersionInfo.Current.GLEXT_EXT_texture_edge_clamp ?
                         (int)GlUtil.Globals.GL_CLAMP_TO_EDGE_EXT  :
                         (int)All.ClampToEdge));
    textureSrgb      = GlUtil.VersionInfo.Current.GLEXT_texture_sRGB;

    if (!textureEdgeClamp && !clampWarned)
    {
        Console.WriteLine("WARNING: No edge clamp extension available. Texture clamping " +
                          "involves the border textel/color and might as they have artifacts.");
        clampWarned = true;
    }

    if (!textureSrgb && !srgbWarned)
    {
        Console.WriteLine("WARNING: No sRGB color space extension available. Colors don't " +
                          "reach the best quality available for display on monitors.");
        srgbWarned = true;
    }

    this.Id = GL.GenTexture();
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.GenTexture");
#endif
    GL.BindTexture(TextureTarget.Texture2D, this.Id);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.BindTexture");
#endif
    GL.TexImage2D  (TextureTarget.Texture2D, 0, this.PxInternalFormat,
                    this.PhysicalWidth, this.PhysicalHeight, 0, this.PxFormat, this.PxType, data);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.TexImage2D");
#endif

    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
                    _isRepeated ? (int)TextureWrapMode.Repeat :
                                  (textureEdgeClamp ? clampToEdge : (int)TextureWrapMode.Clamp));
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.TexParameter-1");
#endif
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
                    _isRepeated ? (int)TextureWrapMode.Repeat :
                                  (textureEdgeClamp ? clampToEdge : (int)TextureWrapMode.Clamp));
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.TexParameter-2");
#endif
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
                    _isSmooth ? (int)TextureMagFilter.Linear : (int)TextureMagFilter.Nearest);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.TexParameter-3");
#endif
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
                    _isSmooth ? (int)TextureMinFilter.Linear : (int)TextureMinFilter.Nearest);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtTexture).Name, methodName, "GL.TexParameter-4");
#endif

    return this.Id;
}

To be sure the GL calls are in the right order and use permitted parameter, I call GlUtil.Globals.CheckError() after every GL API call. To prevent a negative runtime impact, the CheckError() call is compiled for DEBUG mode targets only. The error check looks like:

C#
/// <summary>
/// Check and report details for known GL error types.
/// </summary>
/// <param name="className">The class wherein the error occurs.</param>
/// <param name="methodName">The method name wherein the error occurs.</param>
/// <param name="callName">The call that causes the error.</param>
/// <remarks>
/// With .NET 4.5 it is possible to use equivalents for C/C++ <c>__FILE__</c> and <c>__LINE__</c>.
/// On older .NET versions we use <c>className</c>, <c>methodName</c> and <c>callName</c> instead.
/// </remarks>
internal static void  CheckError(string className, string methodName, string callName)
{
    int errorCode = (int)GL.GetError();
    if(errorCode == 0)
        return;

    string error = "Unknown error";
    string description = "No description";

    if (errorCode == GL_INVALID_ENUM)
    {
        error = "GL_INVALID_ENUM";
        description = "An unacceptable value has been specified for an enumerated argument.";
    }
    else if (errorCode == GL_INVALID_VALUE)
    {
        error = "GL_INVALID_VALUE";
        description = "A numeric argument is out of range.";
    }
    else if (errorCode == GL_INVALID_OPERATION)
    {
        error = "GL_INVALID_OPERATION";
        description = "The specified operation is not allowed in the current state.";
    }
    else if (errorCode == GL_STACK_OVERFLOW)
    {
        error = "GL_STACK_OVERFLOW";
        description = "This command would cause a stack overflow.";
    }
    else if (errorCode == GL_STACK_UNDERFLOW)
    {
        error = "GL_STACK_UNDERFLOW";
        description = "This command would cause a stack underflow.";
    }
    else if (errorCode == GL_OUT_OF_MEMORY)
    {
        error = "GL_OUT_OF_MEMORY";
        description = "There is not enough memory left to execute the command.";
    }
    else if (errorCode == GL_INVALID_FRAMEBUFFER_OPERATION)
    {
        error = "GL_INVALID_FRAMEBUFFER_OPERATION";
        description = "The object bound to FRAMEBUFFER_BINDING is not 'framebuffer complete'.";
    }
    else if (errorCode == GL_CONTEXT_LOST)
    {
        error = "GL_CONTEXT_LOST";
        description = "The context has been lost, due to a graphics card reset.";
    }
    else if (errorCode == GL_TABLE_TOO_LARGE)
    {
        error = "GL_TABLE_TOO_LARGE";
        description = "The exceeds the size limit. This is part of the " +
                      "(Architecture Review Board) ARB_imaging extension.";
    }

    Console.WriteLine ("An internal OpenGL call failed in class '" + className + "' " +
                       "method '" + methodName + "' call '" + callName + "'. " +
                       "Error '" + error + "' description: " + description);
}

The errors I evaluate are:

C#
/// <summary>
/// The GL call return value 'NO ERROR'.
/// </summary>
public static int GL_NO_ERROR                        = 0;

/// <summary>
/// The GL call return value 'INVALID ENUM'.
/// </summary>
public static int GL_INVALID_ENUM                    = 0x0500;

/// <summary>
/// The GL call return value 'INVALID VALUE'.
/// </summary>
public static int GL_INVALID_VALUE                   = 0x0501;

/// <summary>
/// The GL call return value 'INVALID OPERATION'.
/// </summary>
public static int GL_INVALID_OPERATION               = 0x0502;

/// <summary>
/// The GL call return value 'STACK OVERFLOW'.
/// </summary>
public static int GL_STACK_OVERFLOW                  = 0x0503;

/// <summary>
/// The GL call return value 'STACK UNDERFLOW'.
/// </summary>
public static int GL_STACK_UNDERFLOW                 = 0x0504;

/// <summary>
/// The GL call return value 'OUT OF MEMORY'.
/// </summary>
public static int GL_OUT_OF_MEMORY                   = 0x0505;

/// <summary>
/// The GL call return value 'INVALID FRAMEBUFFER OPERATION'.
/// </summary>
public static int GL_INVALID_FRAMEBUFFER_OPERATION   = 0x0506;

/// <summary>
/// The GL call return value 'CONTEXT LOST'.
/// </summary>
public static int GL_CONTEXT_LOST                    = 0x0507;

/// <summary>
/// The GL call return value 'TABLE TOO LARGE'.
/// This is part of the (Architecture Review Board) ARB_imaging extension.
/// </summary>
public static int GL_TABLE_TOO_LARGE                 = 0x8031;

The actual preserve and resize task is realized by the following code. The current texture metadata are stored within _glyphsTexture.

C#
...
    // Ensure to perform two-dimensional texturing (unless three-dimensional or
    // cube-mapped texturing is also enabled).
    GL.Enable(EnableCap.Texture2D);

    // Copy is done here via PBO (pixel buffer object);
    int pboID;
    GL.GenBuffers(1, out pboID);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.GenBuffers");
#endif
    // Select the target "buffer" PBO.
    GL.BindBuffer(BufferTarget.PixelPackBuffer, pboID);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-1");
#endif
    // Allocate the target "buffer" PBO pixel memory (by delivering a 'System.IntPtr.Zero'
    // pointer).
    System.IntPtr data = System.IntPtr.Zero;
    GL.BufferData(BufferTarget.PixelPackBuffer, (System.IntPtr)_glyphsTexture.RequiredMemoryUsage,
        data, BufferUsageHint.StreamDraw);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BufferData");
#endif
    // Select the source "texture".
    GL.BindTexture(TextureTarget.Texture2D, _glyphsTexture.Id);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindTexture");
#endif
    // The 'pixels' are not a pointer to client memory but an offset in bytes into target "buffer"
    // PBO, if target is GL_PIXEL_PACK_BUFFER!
    System.IntPtr pixels = System.IntPtr.Zero;
    // Return the currently selected source into the currently selected target, which copies
    // source "texture" into "buffer" PBO.
    GL.GetTexImage(TextureTarget.Texture2D, 0, _glyphsTexture.PxFormat, _glyphsTexture.PxType,
        pixels);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.GetTexImage");
#endif
    // The "buffer" PBO must be unbound before the target "texture" creation.
    GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-2");
#endif

    // Create the target "texture" - create metadata.
    FtTexture newGlyphsTexture = new FtTexture(_glyphsTexture.PhysicalWidth,
        _glyphsTexture.PhysicalHeight * 2, 0, 2);
    // Create the target "texture" - set pixel format synchronously to the source.
    newGlyphsTexture.SetPixelFormat(_glyphsTexture.PxInternalFormat, _glyphsTexture.PxFormat,
        _glyphsTexture.PxType);
    // Create the target "texture" - create and bind the physical texture.
    newGlyphsTexture.CreateAndBindPhysicalTexture(IntPtr.Zero);

    // Select the source "buffer" PBO.
    GL.BindBuffer(BufferTarget.PixelUnpackBuffer, pboID);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-3");
#endif
    // Return the currently selected source into the currently selected target, which copies
    // "buffer" PBO into target "texture".
    GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, _glyphsTexture.PhysicalWidth,
        _glyphsTexture.PhysicalHeight, newGlyphsTexture.PxFormat, newGlyphsTexture.PxType,
        System.IntPtr.Zero);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.TexSubImage2D");
#endif
    // Unbind the "buffer" PBO.
    GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-4");
#endif
    // Free the "buffer" PBO.
    GL.DeleteBuffers(1, new int[] {pboID});
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.DeleteBuffers");
#endif
    // Free source "texture".
    GL.DeleteTextures (1, new int[] {_glyphsTexture.Id});
#if DEBUG
    GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.DeleteTextures");
#endif

    // Done with two-dimensional texturing.
    GL.Enable(EnableCap.Texture2D);
    // Logically replace source "texture" with target "texture".
    _glyphsTexture = newGlyphsTexture;
...

That's it!

Points of Interest

I hope this will help you in finding a solution that fits your needs.

Now I can go on to rework my FreeType based text renderers "FreeTypeGlyphWise" und "FreeTypeLineWise" and hope I can provide a faster solution soon.

History

  • 10th October, 2018 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)