Implementing The Stages
In the last section we decomposed the general GPGPU process into six bite sized concepts. Now, we will write code for five of the concepts, with the rasterization step being done automatically by the gpu. Of course we will organize the code into clear, modular, highly reusable functions. Like LEGOs we can then build our own worlds by assembling these functions according to our own designs.
We put the methods within the GPGPUtility class.
A review of the constructor provides the context in which our functions are defined. Specifically, the functions
will use the instance variables defined here such as the height
, width
and
gl
,
the rendering context.
/**
* Set of functions to facilitate the setup and execution of GPGPU tasks.
*
* @param {integer} width_ The width (x-dimension) of the problem domain.
* Normalized to s in texture coordinates.
* @param {integer} height_ The height (y-dimension) of the problem domain.
* Normalized to t in texture coordinates.
*
* @param {WebGLContextAttributes} attributes_ A collection of boolean values to enable or disable various WebGL features.
* If unspecified, STANDARD_CONTEXT_ATTRIBUTES are used.
* @see STANDARD_CONTEXT_ATTRIBUTES
* @see{@link https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.2}
*/
GPGPUtility = function (width_, height_, attributes_)
{
var attributes;
var canvas;
/** @member {WebGLRenderingContext} gl The WebGL context associated with the canvas. */
var gl;
var canvasHeight, canvasWidth;
var problemHeight, problemWidth;
var standardVertexShader;
var standardVertices;
/** @member {Object} Non null if we enable OES_texture_float. */
var textureFloat;
⋮
⋮ The code in the following paragraphs goes here.
⋮
canvasHeight = height_;
problemHeight = canvasHeight;
canvasWidth = width_;
problemWidth = canvasWidth;
attributes = typeof attributes_ === 'undefined' ? ns.GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES : attributes_;
canvas = this.makeGPCanvas(canvasWidth, canvasHeight);
gl = this.getGLContext();
// Attempt to activate the extension, returns null if unavailable
textureFloat = gl.getExtension('OES_texture_float');
};
// Disable attributes unused in computations.
GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES = { alpha: false, depth: false, antialias: false };
The Canvas
Even in this first step we see something different from normal WebGL. We create the canvas, but we don't attach the canvas to the DOM. You only need to attach the canvas to the DOM when you want to render it to the screen. Many purely computational problems will never be rendered to the screen.
/**
* Create a canvas for computational use. Computations don't
* require attachment to the DOM.
*
* @param {integer} width The width (x-dimension) of the problem domain.
* @param {integer} height The height (y-dimension) of the problem domain.
*
* @returns {HTMLCanvasElement} A canvas with the given height and width.
*/
this.makeGPCanvas = function (width, height)
{
var canvas;
canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
When we fetch the WebGL context we provide a set of attributes to disable WebGL features that we will not use. We disable a standard set of features if no explicit one is provided.
// Disable attributes unused in computations.
GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES = { alpha: false, depth: false, antialias: false };
/**
* Get a 3d context, webgl or experimental-webgl. The context presents a
* javascript API that is used to draw into it. The webgl context API is
* very similar to OpenGL for Embedded Systems, or OpenGL ES.
*
* @returns {WebGLRenderingContext} A manifestation of OpenGL ES in JavaScript.
*/
this.getGLContext = function ()
{
// Only fetch a gl context if we haven't already
if(!gl)
{
gl = canvas.getContext("webgl", attributes)
|| canvas.getContext('experimental-webgl', attributes);
}
return gl;
};
The Geometry
The simplest geometry that covers the canvas is a rectangle with one vertex at each corner. Luckily, in normalized device coordinates the corners of the canvas are at (-1, -1), (1, -1), (1, 1), and (-1, 1). Our vertex shader passes these coordinates on without any projection or other modifications.
Remember that the problem grid exactly matches the canvas pixels and the texture elements. This means that we also want to attach the texture at the canvas corners. This can be a bit confusing because texture coordinates are still another coordinate system to work with. The texture coordinates range from (0, 0), which we place on the (-1, -1) vertex to (1, 1), which we place on the (1, 1) vertex.
/**
* Return a standard geometry with texture coordinates for GPGPU calculations.
* A simple triangle strip containing four vertices for two triangles that
* completely cover the canvas. The included texture coordinates range from
* (0, 0) in the lower left corner to (1, 1) in the upper right corner.
*
* @returns {Float32Array} A set of points and textures suitable for a two triangle
* triangle fan that forms a rectangle covering the canvas
* drawing surface.
*/
this.getStandardGeometry = function ()
{
// Sets of x,y,z(=0),s,t coordinates.
return new Float32Array([-1.0, 1.0, 0.0, 0.0, 1.0, // upper left
-1.0, -1.0, 0.0, 0.0, 0.0, // lower left
1.0, 1.0, 0.0, 1.0, 1.0, // upper right
1.0, -1.0, 0.0, 1.0, 0.0]);// lower right
};
Create A Texture
Normally textures contain pixel color data with eight bits for each of the red, green, blue, and alpha channels so the color for each pixel takes up one 32 bit word. With floating point textures we have a full 32 bit floating point number for each of the RGB and A channels. This allows us to easily store floating point numbers in a texture element. We will hijack this mechanism to store data for our calculations. As we will see, we have to be careful because floating point textures are an optional part of WebGL.
OES_texture_float
is an OpenGL ES, and hence a WebGL, extension. This is
an optional part of OpenGL that may not be present, and in any event will not be active unless
activated with getExtension
.
If the extension is not available, getExtension
returns null
.
Later we will see how to utilize the GPU even when floating point textures are not available.
// Non null if we enable OES_texture_float
var textureFloat;
// Attempt to activate the extension, returns null if unavailable
textureFloat = gl.getExtension('OES_texture_float');
/**
* Check if floating point textures are available. This is an optional feature,
* and even if present are usually not usable as a rendering target.
*/
this.isFloatingTexture = function()
{
return textureFloat != null;
};
Now that we have enabled floating point textures, let's build one. We create the
texture with createTexture
, then bind it with bindTexture
,
which makes it the currently active texture.
We set several options to make textures more usable for holding computational data.
Set both TEXTURE_MIN_FILTER
and TEXTURE_MAG_FILTER
to
NEAREST
to prevent any issue with the texture being smaller or larger
then the geometry. These come into play when we map the texture to a geometry that
is smaller or larger, respectively, than the texture. Remember though, that we keep
the canvas, geometry and texture sizes in sync so we don not expect this to come into
play. In any case, we never want interpolation between texture values.
If we read a value beyond the edge of the texture, we want the value from the edge. We set
this up with both TEXTURE_WRAP_S
and TEXTURE_WRAP_T
set to
CLAMP_TO_EDGE
. Remember that
texImage2D
sets up the format, and optionally the data for the texture.
This time we will setup an RGBA floating point texture. This means that each of the
R,G,B and A channels contains a floating point number. For now we will only use one,
but later we will see we can make some significant performance optimizations by making
use of all four channels.
/**
* Create a width x height texture of the given type for computation.
* Width and height must be powers of two.
*
* @param {WebGLRenderingContext} The WebGL context for which we will create the texture.
* @param {integer} width The width of the texture in pixels. Normalized to s in texture coordinates.
* @param {integer} height The height of the texture in pixels. Normalized to t in texture coordinates.
* @param {number} type A valid texture type. FLOAT, UNSIGNED_BYTE, etc.
* @param {number[] | null} data Either texture data, or null to allocate the texture but leave the texels undefined.
*
* @returns {WebGLTexture} A reference to the created texture on the GPU.
*/
this.makeTexture = function (gl, width, height, type, data)
{
var texture;
// Create the texture
texture = gl.createTexture();
// Bind the texture so the following methods effect this texture.
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Pixel format and data for the texture
gl.texImage2D(gl.TEXTURE_2D, // Target, matches bind above.
0, // Level of detail.
gl.RGBA, // Internal format.
width, // Width - related to s on textures.
height, // Height - related to t on textures.
0, // Always 0 in OpenGL ES.
gl.RGBA, // Format for each pixel.
type, // Data type for each chanel.
data); // Image data in the described format, or null.
// Unbind the texture.
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
};
Output Texture
Normally the results from the fragment shader are drawn to the screen, and the
value of gl_FragColor
is the color of a pixel. We, however, capture
gl_FragColor
into a texture element (texel). We can even know which
texel we are assigning a value to because we keep the size and position of the
canvas, geometry, and textures aligned.
The
canvas.width
. Similarly the
canvas.height
. This lets us know some important things about the
texture.
The
Within the fragment shader we will have access to the texture coordinates, and from this we know exactly which texel we are writing to. Specifically,
Finally, we get to setup the texture as our rendering target. OpenGL always renders to a framebuffer. We create our own framebuffer object (FBO), then bind it to make it the target of framebuffer operations such as rendering or attaching a texture for offscreen rendering to a texture as we want for GPGPU processing.
/**
* Create and bind a framebuffer, then attach a texture.
*
* @param {WebGLRenderingContext} gl The WebGL context associated with the framebuffer and texture.
* @param {WebGLTexture} texture The texture to be used as the buffer in this framebuffer object.
*
* @returns {WebGLFramebuffer} The framebuffer
*/
this.attachFrameBuffer = function (gl, texture)
{
var frameBuffer;
// Create a framebuffer
frameBuffer = gl.createFramebuffer();
// Make it the target for framebuffer operations - including rendering.
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
// Our texture is the target of rendering ops now.
gl.framebufferTexture2D(gl.FRAMEBUFFER, // The target is always a FRAMEBUFFER.
gl.COLOR_ATTACHMENT0, // We are providing the color buffer.
gl.TEXTURE_2D, // This is a 2D image texture.
texture, // The texture.
0); // 0, we aren't using MIPMAPs
return frameBuffer;
};
To ensure that our framebuffer object is usable, we must also call
checkFramebufferStatus
. If this returns anything other than
FRAMEBUFFER_COMPLETE
then the framebuffer setup has failed.
This is especially important because we have assumed that we can render
to a floating point texture. On platforms where that is not allowed, this
the the earliest point where that can be determined. Indeed, that is the
most common cause for a framebuffer incomplete response.
/**
* Check the framebuffer status. Return false if the framebuffer is not complete,
* That is if it is not fully and correctly configured as required by the current
* hardware. True indicates that the framebuffer is ready to be rendered to.
*
* @returns {boolean} True if the framebuffer is ready to be rendered to. False if not.
*/
this.frameBufferIsComplete = function ()
{
var message;
var status;
var value;
status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
switch (status)
{
case gl.FRAMEBUFFER_COMPLETE:
value = true;
break;
case gl.FRAMEBUFFER_UNSUPPORTED:
message = "Framebuffer is unsupported";
value = false;
break;
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
message = "Framebuffer incomplete attachment";
value = false;
break;
case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
message = "Framebuffer incomplete (missmatched) dimensions";
value = false;
break;
case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
message = "Framebuffer incomplete missing attachment";
value = false;
break;
default:
message = "Unexpected framebuffer status: " + status;
value = false;
}
return {isComplete: value, message: message};
};