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.
public int CreateAndBindPhysicalTexture(System.IntPtr data)
{
#if DEBUG
string methodName = "CreateAndBindPhysicalTexture";
#endif
GlUtil.VersionInfo.Current.EnsureExtensionsInit();
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:
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:
public static int GL_NO_ERROR = 0;
public static int GL_INVALID_ENUM = 0x0500;
public static int GL_INVALID_VALUE = 0x0501;
public static int GL_INVALID_OPERATION = 0x0502;
public static int GL_STACK_OVERFLOW = 0x0503;
public static int GL_STACK_UNDERFLOW = 0x0504;
public static int GL_OUT_OF_MEMORY = 0x0505;
public static int GL_INVALID_FRAMEBUFFER_OPERATION = 0x0506;
public static int GL_CONTEXT_LOST = 0x0507;
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
.
...
GL.Enable(EnableCap.Texture2D);
int pboID;
GL.GenBuffers(1, out pboID);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.GenBuffers");
#endif
GL.BindBuffer(BufferTarget.PixelPackBuffer, pboID);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-1");
#endif
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
GL.BindTexture(TextureTarget.Texture2D, _glyphsTexture.Id);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindTexture");
#endif
System.IntPtr pixels = System.IntPtr.Zero;
GL.GetTexImage(TextureTarget.Texture2D, 0, _glyphsTexture.PxFormat, _glyphsTexture.PxType,
pixels);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.GetTexImage");
#endif
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-2");
#endif
FtTexture newGlyphsTexture = new FtTexture(_glyphsTexture.PhysicalWidth,
_glyphsTexture.PhysicalHeight * 2, 0, 2);
newGlyphsTexture.SetPixelFormat(_glyphsTexture.PxInternalFormat, _glyphsTexture.PxFormat,
_glyphsTexture.PxType);
newGlyphsTexture.CreateAndBindPhysicalTexture(IntPtr.Zero);
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, pboID);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-3");
#endif
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
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.BindBuffer-4");
#endif
GL.DeleteBuffers(1, new int[] {pboID});
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.DeleteBuffers");
#endif
GL.DeleteTextures (1, new int[] {_glyphsTexture.Id});
#if DEBUG
GlUtil.Globals.CheckError(typeof(FtFont).Name, "ctr", "GL.DeleteTextures");
#endif
GL.Enable(EnableCap.Texture2D);
_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