Texture Storage

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.

Anatomy of storage

A texture's image storage 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 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

ArrayLayers

CubemapFaces

Imagedimensionality

GL_TEXTURE_1D

Yes

1D

GL_TEXTURE_2D

Yes

2D

GL_TEXTURE_3D

Yes

3D

GL_TEXTURE_1D_ARRAY

Yes

Yes

1D

GL_TEXTURE_2D_ARRAY

Yes

Yes

2D

GL_TEXTURE_CUBE_MAP

Yes

Yes

2D

GL_TEXTURE_CUBE_MAP_ARRAY

Yes

Yes

Yes

2D

GL_TEXTURE_RECTANGLE

2D

GL_TEXTURE_BUFFER

1D

GL_TEXTURE_2D_MULTISAMPLE

2D

GL_TEXTURE_2D_MULTISAMPLE_ARRAY

Yes

2D

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.

Image sizes

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 mipmap level. "Maximum" in level index value, because the base level starts at 0.

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.

Warning: Cube map and cube map array textures must use square sizes. The width and height must be the same.

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.

One of the big differences between mutable storage allocation and immutable storage allocation 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.

Immutable storage

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.

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.

Texture views

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:

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​. When this function completes, it will share immutable 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 or faces (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. Cube maps are treated as an array texture with 6 layers. Cube map array layers here are layer-faces.

There are two special tricks you can play 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 original 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:

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:

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:

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*​.

Mutable storage

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.

Texture completeness

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 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.

Note: This is a very abbreviated discussion of texture completeness rules. It is possible to not set the mipmap range and still allow the texture to be complete by turning off mipmap sampling or using a Sampler Object that doesn't do mipmap-based sampling. However, following the above rules will always result in a complete texture, no matter what. So you should follow the rules unless you have a good reason not to.

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:

GL_TEXTURE_CUBE_MAP_POSITIVE_X

GL_TEXTURE_CUBE_MAP_NEGATIVE_X

GL_TEXTURE_CUBE_MAP_POSITIVE_Y

GL_TEXTURE_CUBE_MAP_NEGATIVE_Y

GL_TEXTURE_CUBE_MAP_POSITIVE_Z

GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

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.

Direct creation

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.

The most direct method performs a regular Pixel Transfer operation from either client memory or a buffer object. These are the functions that allocate and upload in this way:

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, target​s 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.

Warning: Do not forget to make sure that all mipmaps and images in the same texture are allocated with the same internalformat​.

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.

Note: Even if data is NULL, the format and type fields must be legal fields, or the entire call will fail with a GL_INVALID_ENUMerror.

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:

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.

Warning:data​cannot be NULL here; if you didn't want to transfer pixel data, you should have used glTexImage*​.

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:

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).

Note: Copies to multisample textures are not allowed. This is because glReadPixels​ forces a multisample resolve. You can copy from multisample images, but this will do a resolve. If you want to preserve the sample count, you must blit it into an already existing multisample texture's storage.

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.

Storage contents

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.

Automatic mipmap generation

It is often useful to auto-generate a mipmap set from just the base mipmap level in the previously defined range. How exactly the implementation does filtering for mipmap generation is implementation-dependent.

Before calling this function, the base mipmap level must be established. It must be allocated (either with mutable or immutable storage). For cube maps, this also means that each of the 6 faces of the base mipmap level must be allocated.

This will cause the texture bound to target​ to have its mipmap levels below the base level auto-generated. Note that the GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL range are observed. The current base level will not be changed; only all of the other levels will be generated. Any mipmap levels defined outside of the base/max range will not be changed.

If GL_TEXTURE_MAX_LEVEL specifies mipmap levels that have not yet been allocated, then they will be allocated. Recall that in an immutable storage texture, GL_TEXTURE_MAX_LEVEL will always specify an allocated mipmap level, so it isn't possible for this to happen for immutable storage textures.

Pixel upload

Part or all of a mipmap level can have its pixels replaced via a Pixel Transfer operation. This can be initiated with these functions:

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).

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 area is actually 4x smaller than the overwritten area in the destination image. In terms of pixels of course; in terms of memory, it's still just a memcpy​).

Framebuffer copy

You can copy image data into existing texture storage with these functions:

The xoffset​, yoffset​, and zoffset​ parameters represent the offset into the destination mipmap level of where to copy the image data. The x​ and y​ parameters are the offset into the framebuffer image being copied from. The width​ and height​ define the size of the rectangle to copy from the framebuffer.

Invalidation

It is possible to tell OpenGL to "invalidate" images stored in a texture. From an outside perspective, this means that the contents of the selected images become undefined; the pixel values can be anything.

However, from a Synchronization perspective, this can be a valuable tool. If you invalidate the storage for a texture, then the OpenGL implementation can simply allocate new storage for it internally. This means that operations on this new storage don't have to wait for operations on the old (now orphaned) storage to complete.

This is perhaps most useful with multisample textures, since these are often used as render targets. Once you've rendered to them and either blitted from them or otherwise resolved them to the screen or some other image, they aren't needed anymore. You still need the texture object, but you can orphan the storage by invalidating it, which lets OpenGL create new storage that you can immediately (hopefully) start rendering into.

To invalidate an entire mipmap level of a texture, you use this function:

The offsets define the position within the given texture to invalidate, and the sizes define the region to invalidate. For textures lacking those dimensions, the offset should be 0 and the missing sizes should be 1. Cube maps are again treated as array textures with 6 layers, so the zoffset​ is the face index of the cube map.

Pixel download

This function performs a Pixel Transfer pack (ie: read) operation into img​. It will retrieve the entire mipmap level of the texture, except for cube maps. For them, you must use the individual face target​s.