The Texture Storage is the part of Texture objects that contains the actual pixel data stored in the texture. This article describes the layout of a texture's storage, the many ways of managing the allocation and pixel contents of a texture's storage.
- 1 Anatomy of storage
- 2 Immutable storage
- 3 Mutable storage
- 4 Storage contents
- 5 Buffer storage
Anatomy of storage
A texture contains one or more images of a certain dimensionality. Each kind of texture has a specific arrangement of images in its storage. Textures can have mipmaps, which are smaller versions of the same image used to aid in texture sampling and filtering. Each mipmap level has a separate set of images.
Since a texture stores multiple images, it is important to be able to identify a specific image in a texture. Each image in a texture can be uniquely identified by the following numbers, depending on the texture type:
- For textures that can have mipmaps, the mipmap level that contains the image.
- For Array Textures, the array layer that contains the image.
- For Cubemap Textures, the face within that array layer and mipmap level. Note that for cubemap array textures, the layer and face are combined into layer-faces.
Therefore, a texture can be thought of as a three-dimensional array of images. The first index is the mipmap level, the second is the array layer, and the third is the cube map face. Another way to think of it is that a texture has a number of mipmap levels. Each mipmap can have a number of array layers. And each array has a number of faces. Face, layer, and level yields a single image.
Here is a table describing which texture types may have which values (mipmaps, array layers, and faces):
|Texture type||Mipmaps||Array Layers||Cubemap Faces||Image dimensionality|
Note that virtually every function in OpenGL that deals with a texture's storage assumes that the texture may have mipmaps. So almost all of them take a level parameter. When using such functions for textures that cannot be mipmapped, the value for level must always be 0. Similarly, when dealing with functions that ask for a number of mipmap levels (such as the function to create storage for a texture), you must use 1 for non-mipmapped texture types.
Each texture type represents images of a certain dimensionality. As such, it is important to know the size of the individual images within a texture. This is easy enough.
All images that have the same mipmap level (ie: all array layers and/or cube map faces in a mipmap) in a texture will have the same size (note that there are ways to try to break this rule; they will only lead to a non-functional texture). That size depends on the size of the base mipmap level of the texture: level 0. The size of level 0 images defines the texture's effective size.
For every mipmap level past level 0, the size decreases in half, rounded down. So if you have a 67x67 base mipmap level for a texture with two-dimensional images, the images in mipmap level 1 will be 33x33 in size. For level 2, they will be 16x16. And so forth.
The mipmap chain stops when all dimensions are 1; that is the maximum (in value, since the base level is 0) mipmap level.
The number of array layers and cube map faces do not change with the mipmap level. If a texture has 3 array layers, every mipmap will have 3 array layers. This is important to remember when allocating texture storage and uploading pixel data.
Kinds of storage
The above describes the way the storage exists within a texture. How to create that storage is another matter.
There are three kinds of storage for textures: mutable storage, immutable storage, and buffer storage. Only Buffer Textures can use buffer storage, where the texture gets its storage from a Buffer Object. And similarly, buffer textures cannot use mutable or immutable storage. Any other kind of texture can use either mutable or immutable storage. Because of this, the discussion below, with the exception of one section, will focus on mutable and immutable storage.
The difference between mutable storage and immutable storage is this: immutable storage allocates all of the images for the texture all at once. Every mipmap level, array layer, and cube map face is all allocated with a single call, giving all of these images a specific Image Format. It is called "immutable" because once the storage is allocated, the storage cannot be changed. The texture can be deleted as normal, but the storage cannot be altered. A 256x256 2D texture with 5 mipmap layers that uses the GL_RGBA8 image format will *always* be a 256x256 2D texture with 5 mipmap layers that uses the GL_RGBA8 image format.
Note that what immutable storage refers to is the allocation of the memory, not the contents of that memory. You can upload different pixel data to immutable storage all you want. With mutable storage, you can re-vamp the storage of a texture object entirely, changing a 256x256 texture into a 1024x1024 texture.
|Core in version||4.5|
|Core since version||4.2, 4.3|
|Core ARB extension||ARB_texture_storage, ARB_texture_storage_multisample|
Allocating immutable storage for a texture requires binding the texture to its target, then calling a function of the form glTexStorage*. Which function you call depends on which texture type you are trying to allocate storage for. Each function only works on a specific set of targets.
void glTexStorage1D( GLenum target, GLint levels, GLint internalformat, GLsizei width );
- Valid target: GL_TEXTURE_1D
void glTexStorage2D( GLenum target, GLint levels, GLint internalformat, GLsizei width, GLsizei height );
- Valid targets: GL_TEXTURE_2D, GL_TEXTURE_RECTANGE, GL_TEXTURE_CUBEMAP, or GL_TEXTURE_1D_ARRAY.
- For 1D array textures, the number of 1D images that each mipmap level has is the height value. For rectangle textures, the number of mipmap levels must be 1.
void glTexStorage3D( GLenum target, GLint levels, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth );
- Valid targets: GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_ARRAY (this requires GL 4.0 or ARB_texture_cube_map_array)
- For 2D array textures, the number of 2D images that each mipmap level has is the depth value.
- For 2D cubemap array textures, the number of cubemap layer-faces is the depth, which must be a multiple of 6. Therefore, the number of individual cubemaps in the array is given by depth / 6.
void glTexStorage2DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations );
- Valid targets: GL_TEXTURE_2D_MULTISAMPLE
void glTexStorage3DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations );
- Valid targets: GL_TEXTURE_3D_MULTISAMPLE
These functions allocate images with the given size (width, height, and depth, where appropriate), with the number of mipmaps given by levels. The storage is created here, but the contents of that storage is undefined. It's a lot like calling malloc; you get memory, but there's nothing in it yet.
The internalformat parameter defines the Image Format to use for the texture. For the most part, any texture type can use any image format. Including the compressed formats. Note that these functions explicitly require the use of sized image formats. So GL_RGBA is not sufficient; you have to ask for a size, like GL_RGBA8.
For the multisample functions, samples defines the number of samples that will be used per-texel in the texture. If you set fixedsamplelocations is GL_TRUE, then the following is assured:
- The texels in the image will all use the same sample locations.
- The texels in the image will all use the same number of sample locations (normally, the implementation could give some texels fewer than samples, while other texels get more).
- All textures with fixed sample locations will use the same set of sample locations, regardless of Image Format.
|Core in version||4.5|
|Core since version||4.3|
|Core ARB extension||ARB_texture_view|
Besides the infinitely cleaner texture specification syntax and the general reduction in the chance for mistakes, creating immutable storage for textures has one other advantage: immutable storage can be shared between texture objects.
Mutable storage is bound to a single texture object. Immutable storage can be shared among several objects, such that they are all referring to the same memory. Think of it like passing a reference-counted smart pointer around. Each object has its own smart pointer, and the memory doesn't go away until all objects that reference the shared memory are destroyed.
The glTexStorage* functions all create new immutable storage, ala malloc. In order to share previously-created immutable storage, we must use a different function:
void glTextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers)
This function takes two textures. origtexture is the texture that currently has immutable storage. texture is a new texture that doesn't have immutable storage. target is the type of texture. Then this function completes, it will now share storage with origtexture. This is called a "view texture", because the new texture represents a "view" into the original texture's storage.
The view texture does not have to look at the exact same size of storage. It can reference only a portion of the original texture. For example, if you have an immutable texture with 6 mipmap levels, you can create a view that only uses 3 mipmap levels.
This is the responsibility of the minlevel and numlevels parameters. The minlevel specifies the mipmap level in the origtexture that will become the base level of the view texture. numlevels specifies how many mipmaps are to be viewed. If the origtexture is not a texture type that has mipmaps (multisample or rectangle textures), then minlevel must be 0 and numlevels must be 1. For textures that could have mipmaps, then minlevel and numlevels will be clamped to the actual available number of mipmaps in the source texture (though it is an error if minlevel is outside of the range of mipmaps).
For textures that have layers (GL_TEXTURE_1D_ARRAY, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP, or GL_TEXTURE_CUBE_MAP_ARRAY), a range of layers to take can be specified with minlayer and numlayers. As with the mipmap level range parameters, the layer ranges are clamped to the available range of layers, and minlayer must be an available layer in the image.
There are two special tricks you can do with view textures. The texture type of origtexture does not have to match the target. For example, you can have a 1D array texture and create a view of it as a 1D texture, which represents a specific array layer of the texture. To do this, you must use minlayer to define the layer you want to select, and pass 1 to numlayers.
You can only perform this kind of conversion between very specific sets of texture types. Here is a table defining where the conversion is allowed:
|Original Target||Compatible New Targets|
|GL_TEXTURE_CUBE_MAP||GL_TEXTURE_CUBE_MAP, GL_TEXTURE_2D, GL_TEXTURE_2D_ARRAY, GL_TEXTUER_CUBE_MAP_ARRAY|
|GL_TEXTURE_BUFFER||none. Cannot be used with this function.|
|GL_TEXTURE_2D_ARRAY||GL_TEXTURE_2D, GL_TEXTURE_CUBE_MAP, GL_TEXTURE_2D_ARRAY, GL_TEXTUER_CUBE_MAP_ARRAY|
|GL_TEXTURE_CUBE_MAP_ARRAY||GL_TEXTURE_CUBE_MAP, GL_TEXTURE_2D, GL_TEXTURE_2D_ARRAY, GL_TEXTUER_CUBE_MAP_ARRAY|
The number of mipmaps levels and array layers you fetch are not allowed to violate the constraints of the destination target. So if you want to get a rectangle texture view of a 2D texture, you must pick exactly one mipmap level because Rectangle Textures can only have one mipmap.
The other trick you can do with view textures is change the Image Format. internalformat is not restricted to the exact image format that origtexture uses. It simply must be compatible with it. Here is a chart explaining which formats are compatible with which other formats:
|128-bit||GL_RGBA32F, GL_RGBA32UI, GL_RGBA32I|
|96-bit||GL_RGB32F, GL_RGB32UI, GL_RGB32I|
|64-bit||GL_RGBA16F, GL_RG32F, GL_RGBA16UI, GL_RG32UI, GL_RGBA16I, GL_RG32I, GL_RGBA16, GL_RGBA16_SNORM|
|48-bit||GL_RGB16, GL_RGB16_SNORM, GL_RGB16F, GL_RGB16UI, GL_RGB16I|
|32-bit||GL_RG16F, GL_R11F_G11F_B10F, GL_R32F, GL_RGB10_A2UI, GL_RGBA8UI, GL_RG16UI, GL_R32UI, GL_RGBA8I, GL_RG16I, GL_R32I, GL_RGB10_A2, GL_RGBA8, GL_RG16, GL_RGBA8_SNORM, GL_RG16_SNORM, GL_SRGB8_ALPHA8, GL_RGB9_E5|
|24-bit||GL_RGB8, GL_RGB8_SNORM, GL_SRGB8, GL_RGB8UI, GL_RGB8I|
|16-bit||GL_R16F, GL_RG8UI, GL_R16UI, GL_RG8I, GL_R16I, GL_RG8, GL_R16, GL_RG8_SNORM, GL_R16_SNORM|
|8-bit||GL_R8UI, GL_R8I, GL_R8, GL_R8_SNORM|
Any formats not on this chart are only compatible with themselves; you cannot create a view with a different format.
Because view textures reference immutable storage, this also means that view textures can be used as origtexture. So you can create a view of a view.
The mipmap levels, number and indices of layers, base level texture size, and similar parameters for a view are defined by the particular view, not the original block of storage. As an example, let's say we create a 2D array texture with immutable storage as follows:
glBindTexture(GL_TEXTURE_2D_ARRAY, tex); glTexStorage3D(GL_TEXTURE_2D_ARRAY, 10, GL_RGBA8, 1024, 1024, 6);
We can create a view of tex:
glTextureView(texView1, GL_TEXTURE_2D_ARRAY, tex, GL_RGBA8, 2, 5, 1, 3);
texView1 is a 2D array texture. As far as texView1 is concerned, the size of its base level is 256x256, because it starts with the third mipmap of tex. It has only 5 mipmaps. And though it is an array, it has only 3 array layers.
We can create view from the new texture:
glTextureView(texView2, GL_TEXTURE_2D, texView1, GL_RGBA8, 2, 1, 1, 1);
texView2 is a 2D texture. It has 1 mipmap level, and the size of that level is 64x64, because it picked the third mipmap from texView1. It has 1 array layer, which is taken from the second layer in texView1.
What is texView2 in relation to tex? Exactly what it sounds like. texView2 takes the fifth mipmap level and the third array layer from tex.
A view texture cannot view more mipmap levels and/or array layers than the origtexture advertises, even if the original texture is a view texture and those extra levels/layers exist. Views can only view the same information that the original does or a subset of it. If you want to view more of the storage, you need to use a texture that can access that storage.
Thus, it is technically possible to completely lose access to some levels/layers of a texture, if you delete the original texture created with glTexStorage*.
OpenGL functions of the form gl*TexImage* are used to create mutable storage for images within a texture. Calling any of these on a texture that had immutable storage created for it is an error.
The immutable storage calls are the equivalent of a C malloc: they allocate memory, but they don't put anything in it. All of the mutable storage calls are capable of both allocating memory and transferring pixel data into that memory.
These functions allocate one mipmap layer of the texture at a time (and in some cases, only part of a mipmap layer at a time). Because each mipmap layer is created individually, there are many points of failure when creating mipmapped textures in this way. You must be sure to:
- Use the exact same internal format for each mipmap layer.
- Use the correct size for the mipmap layers. The width/height/depth of a mipmap layer is the width/height/depth of the base layer / 2k, where k is the mipmap level (remember: 0 is the base level). And remember to round down.
- 1D Array Textures have a width and height. But the height specifies the number of elements in the array of 1D textures. Therefore, the height does not change with mipmap levels. Each level uses the same height. The same goes for 2D Array textures and Cubemap Array textures with the depth parameter.
- Allocate all of the mipmap layers you intend to use, then set the base/max levels to match this range.
- For cube maps (but not cube map arrays), allocate each face within a mipmap level using the same size.
Failing to follow the above rules results in a texture that is not "complete" by the rules of the standard. You cannot attempt to sample from such a texture. The best way to avoid completeness problems is to make sure that the texture is always complete; make it complete initially, and leave it that way. That's one of the reasons why immutable storage is nice: such textures are always complete.
Cube map storage
Regular GL_TEXTURE_CUBE_MAPs (and only them. These rules do not apply to GL_TEXTURE_CUBE_MAP_ARRAY) are handled in an unusual way. Normally, each of the functions below will allocate a full mipmap level. All array layers and faces (of cube map arrays) of that mipmap are allocated at once. Not so for non-array cube maps.
For them, you must allocate each face within a mipmap level individually. This is done by playing with the texture target field.
While cube map textures are bound to the GL_TEXTURE_CUBE_MAP target, to reference a specific cube map face within the texture, you use a special target while the cube map is bound to GL_TEXTURE_CUBE_MAP. These targets are:
Each one names a specific face in the cube. Each one is considered a 2D image, so you call the "2D" version of the functions listed below. Note that this is how you interact with (non-array) cube maps for both creating mutable storage and modifying the contents of that storage.
There are several ways to allocate mutable storage; the differences are based on where they get their pixel data from. Since mutable storage creation also uploads data, there are many different places the user can get data from.
The only difference between these groups of functions is where they get the pixel data to initialize their images from.
void glTexImage1D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, void *data ); void glTexImage2D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, void *data ); void glTexImage3D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data ); void glTexImage2DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLboolean ﬁxedsamplelocations ); void glTexImage3DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean ﬁxedsamplelocations );
These functions allocate an image from the texture bound to target (with the previously mentioned cube map target differences). The level parameter specifies the mipmap level for the image to be allocated. You can only allocate one mipmap level at a time.
With the exception of cubemaps, targets for these functions work as they did for the analogous immutable storage functions. So glTexImage2D is used to allocate a mipmap level of a 1D array texture, where the height is the number of elements in the array. Multisample texture types must use the multisample allocation functions. And so forth.
Some of these functions have a border parameter. This was old functionality that is no longer supported (and really, never was); always set it to 0.
The format, type, and data parameters are used for performing a Pixel Transfer operation. This allows one to create a texture and fill it with some data in one call. As with any pixel transfer operation, Pixel Buffer Objects can be used to feed OpenGL the data.
You do not need to fill in the texture's data in the same call that you create it in. If data is NULL, no pixel transfer will be done, and the texture's data is undefined.
The multisample versions of these functions do not offer pixel transfer. This is because the image data of multisample textures cannot be updated from client data. It can only be filled in as a render target or via some other form of in-OpenGL writing operation (Image Load Store, for example).
Compressed format creation
Textures that use compressed image formats need special care. It is perfectly legal to pass an appropriate compressed format to any of the prior functions (except for the multisample ones). However, the Pixel Transfer parameters pose a problem. They are designed for regular image data where each pixel is specified individually.
Most compressed formats store pixels in specially formatted blocks. As such, you cannot perform a direct pixel transfer of previously compressed data. If you use a compressed internalformat with a regular pixel transfer call, you are telling OpenGL to take uncompressed data and compress it manually.
There are a number of special functions for allocating images with compressed formats and simultaneously filling them with compressed data:
void glCompressedTexImage1D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, void *data ); void glCompressedTexImage2D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, void *data ); void glCompressedTexImage3D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, void *data );
With the exception of the last two parameters, these functions work identically to their glTexImage* counterparts. They allocate mutable storage of the given size for the texture bound to the given target.
Where they differ is in how they transfer pixel data. OpenGL assumes that the data you are passing has been properly formatted according to whatever internalformat the image uses. Therefore, it is just going to copy the data verbatim from your data. The imageSize must match with what OpenGL would compute based on the dimensions of the image and the internalformat. If it doesn't, you get an GL_INVALID_VALUE error.
Again, Pixel Buffer Objects work with such transfers. The data parameter must thus be a byte-offset from the front of the buffer bound to GL_UNPACK_BUFFER.
internalformat must not be a generic compressed format. It must be a specific compressed format (such as GL_COMPRESSED_RG_RGTC1 or GL_COMPRESSED_RGB_S3TC_DXT1_EXT.
Framebuffer copy creation
When creating storage for a texture, you can also get the pixel data from the Framebuffer Object or Default Framebuffer currently bound to the GL_READ_FRAMEBUFFER target. It will use the current read buffer of that framebuffer (for color reads), so make sure to use glReadBuffer to set it properly beforehand. Also, the framebuffer must be complete.
These functions act like a combination of glReadPixels followed by glTexImage* for the appropriate type. Since framebuffers are two-dimensional, only 1D and 2D copy creation are allowed:
void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border); void glCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
The width and height define the size of the given mipmap level. They also define the size of the region taken from the framebuffer. x and y define the bottom-left corner where the read starts (remember: OpenGL puts the origin of the framebuffer at the bottom-left).
Which buffer is copied from depends on the type of internalformat. If internalformat is a color format, then the current read buffer specified by glReadBuffer is used. If internalformat has a depth component, the depth buffer is used (an error occurs if there is no depth buffer). If it has a stencil component, the stencil buffer is used (an error occurs if there is no stencil buffer). If internalformat has both depth and stencil, then both are used as the source.
Once the storage has been defined with one of the above functions, the contents of the storage (the actual pixel data) can be modified and access via various functions.
Part or all of a mipmap level can have its pixels replaced via a Pixel Transfer operation. This can be initiated with these functions:
void glTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const GLvoid * data); void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid * data); void glTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const GLvoid * data);
These functions work like their glTexImage* analogs, with two exceptions. First, they can upload to a sub-section of the mipmap level (hence the name "SubImage"). The sub-section is defined by the xoffset, yoffset, zoffset, width, height, and depth parameters. The offsets define the pixel offsets from the bottom-left of the particular mipmap level. The sizes define both the size of the data being transfer and the pixel size of the data to be overwritten.
The second difference is of course that they do not reallocate the texture's storage. They only upload data to the image(s).
|Core in version||4.5|
|Core since version||4.3|
|Core ARB extension||ARB_copy_image|
Image data can be copied between images in textures. To do this, use this function:
void glCopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
The srcName and dstName are the textures to copy from and to, respectively. They can be the same if you wish to copy data between mipmap levels of the same texture.
The srcX, srcY, and srcZ define the offset from the bottom-left of the source mipmap level srcLevel to begin copying. The srcWidth, srcHeight, and srcDepth define the size to be copied. And dstX, GLint dstY, and dstZ specify the offset in the destination mipmap level dstLevel.
Note that GL_TEXTURE_CUBE_MAP textures work differently here. This function treats GL_TEXTURE_CUBE_MAP as basically a GL_TEXTURE_2D_ARRAY texture with 6 layers. So you can copy all of the faces from one cubemap to another by using a srcDepth of 6. The order of the faces in the array is the same as for layered rendering to a cube map.
This function copies pixel data directly as is; no filtering or anything of that nature can take place. It also doesn't do color conversion, so copying from linear RGB to sRGB will not convert the colors. Think of it as memcpy for textures.
This copy operation doesn't do any kind of format conversions either. This means that you can only copy between certain Image Formats. It can copy between image formats that are compatible for texture views.
It can also copy between certain compressed and uncompressed formats. This does not perform compression or decompression; it simply copies the data directly. This allows you to implement compression and decompression algorithms.
For example, you can generate compressed blocks of data into GL_RGBA16UI texture (perhaps through a Compute Shader and Image Load Store). Then you copy the image into a texture who's format is GL_COMPRESSED_RED_RGTC1. Each "pixel" of the source texture represents a compressed block in the destination. So the source rectangle is actually 4x smaller than the overwritten area in the destination image (in terms of pixels. In terms of memory, it's still just a memcpy).
The storage for buffer textures come from Buffer Objects directly. Buffer textures are one-dimensional. Their "width" is the size of the buffer object range that is attached to the texture with glTexBuffer or glTexBufferRange (requires 4.3 or ARB_texture_buffer_range).