OpenGL Shader Image Load Store: Read-Write Textures

The GL_ARB_shader_image_load_store OpenGL extension introduces the ability for GLSL shaders to perform unformatted, random-access reads, writes, and atomic operations directly on texture memory. Prior to this extension, OpenGL shaders could only sample textures as read-only inputs or write to them sequentially through fixed framebuffer attachments. By exposing textures as raw images rather than sampled surfaces, this extension provides the foundational memory architecture required for advanced compute tasks, order-independent transparency, and custom rasterization algorithms.

Image Units Versus Sampler Units

Traditional GLSL pipelines access textures through sampler objects (sampler2D, samplerCube), which are tied to texture image units and process data through a fixed-function pipeline involving filtering, normalized coordinates, and mipmapping.

GL_ARB_shader_image_load_store bypasses this pipeline by introducing dedicated image units and corresponding GLSL image types (such as image2D, iimage3D, or uimage2DArray). Instead of binding with glBindTexture, textures are bound to these image units using the API call:

glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);

This binds an individual mipmap level or layer of a texture directly to an image unit with explicit access permissions (GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE).

Direct Coordinate-Based Addressing

Instead of using normalized floating-point coordinates (ranging from 0.0 to 1.0) and texture filtering, shaders interact with image variables using discrete integer texel coordinates. The extension provides specific built-in GLSL functions for reading and writing:

  • imageLoad(image, P): Reads raw texel data from the image bound to the specified coordinate vector P without applying filtering or interpolation.
  • imageStore(image, P, data): Writes the provided data vector directly to the texel location defined by coordinate P.

Because access is purely coordinate-based, any shader invocation (vertex, fragment, compute, or tessellation) can read from or write to any arbitrary location in the texture buffer, breaking the constraint where a fragment shader invocation could only write to its own assigned pixel.

Format Layout Qualifiers

Because the hardware must understand the underlying binary memory layout during direct memory operations, image variables in GLSL must explicitly declare an image format layout qualifier (unless specific extension subsets allow format-less access for write-only targets).

layout(rgba32f, binding = 0) uniform image2D myImage;

This qualifier dictates how the data is packed in GPU memory, ensuring that imageLoad and imageStore translate vector types to and from the correct binary layout.

Atomic Operations

Allowing multiple parallel shader invocations to read and write to the same memory addresses introduces potential data races. To resolve this, the extension introduces atomic built-in functions that operate on 32-bit signed and unsigned integer image formats (r32i and r32ui).

Functions such as imageAtomicAdd, imageAtomicMin, imageAtomicMax, imageAtomicAnd, and imageAtomicCompSwap guarantee that read-modify-write cycles on a single texel execute atomically across all concurrent shader threads. This capability is critical for building linked lists, histogram binning, and mutex locks directly on the GPU.

Memory Model and Synchronization

Direct read-write access requires explicit memory synchronization because modern GPUs utilize caching hierarchies that do not guarantee cross-invocation coherency by default. The extension addresses this through qualifiers and synchronization primitives:

  1. Memory Qualifiers: Variables can be decorated with coherent, volatile, or restrict. The coherent qualifier ensures that writes performed by one invocation become visible to other invocations once synchronized.
  2. GLSL Barriers: Inside the shader, execution and memory flow are coordinated using barrier functions such as memoryBarrier() or memoryBarrierImage(), which ensure that prior image stores complete before subsequent reads.
  3. API Barriers: Between rendering passes or dispatch calls, the CPU application must invoke glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) to flush caches, ensuring that image stores from one draw call are fully written before subsequent draw calls access the same texture memory.