2.2 Tuples

Tuples are short arrays with elements of the same basic type. Tuples have a semantic type, a size, and a storage type. For instance, an ShPoint3f is a 3D geometric point stored using three single-precision floating-point numbers.

2.2.1 Semantic Types

The generic semantic type is ShAttrib, but semantic types ShPoint, ShNormal, ShVector, ShPlane and ShTexCoord are also supported. The special semantic type ShPosition may be used inside shaders to indicate the special input/output slots for vertex and fragment positions, but this type is otherwise equivalent to a point. See Table 2.1 for a listing of semantic types.

There are actually few differences in how library functions and operators treat the different semantic types. Sh has intentionally been defined not to overly restrict operations on tuples based on semantic type. For instance, addition and scalar multiplication of the ShPoint semantic type is supported in Sh, even though it is geometrically invalid. This is because because affine combinations of points are valid and are built up out of these operations. Some operators do cause type conversions, for example subtracting two points gives a vector, but most do not.

Operations generally act the same on all semantic types as well, for instance the arithmetic operators “+”, “-”, “*” and “/” act componentwise on all basic semantic types. However, there are a few exceptions that would otherwise be errors in which the semantic types are treated differently (for instance, inferral of homogeneous coordinates), and in some cases (e.g. matrices) the multiplication and division operators are specialized. These issues are dealt with at greater length in Chapter 3.




Semantic Types


Name Meaning


ShAttrib generic tuple
ShColor color
ShVector direction
ShNormal covector (tangent plane orientation)
ShPoint nonhomogeneous position
/ E  ShPlanehalfspace equation
ShTexCoordnonhomogeneous texture coordinate
ShPositionposition (special point)



Table 2.1: Semantic types. A semantic type indicates what kind of geometric or numerical object a tuple represents. All semantics types are subclasses of the ShAttrib type.

2.2.2 Storage Types

Storage types indicate how the numeric values stored in a tuple are represented internally. The storage type is indicated by a suffix that roughly mimics (but does not duplicate) the conventions of OpenGL. The available storage types are shown in Table 2.2.


Floating-Point Types
Signed Integer Types
Unsigned Integer Types
Signed Fraction Types
Unsigned Fraction Types





Suffix
Name
Sign/Exponent/Significand















/ E  d double-precision float 1/8/15to1/11/52
f single-precision float 1/8/15to1/8/23
/ E  h half-precision float 1/5/10to1/8/23















/ E  i signed integer 1//15to1//31
/ E  s signed short integer 1//10to1//15
/ E  b signed byte 1//7















/ E  ui unsigned integer 0//15to0//32
/ E  us unsigned short integer 0//10to0//16
/ E  ub unsigned 8-bit byte 0//8















/ E  fi signed fraction in [-1,1] 1//15to1//31
/ E  fs signed short fraction in [-1,1] 1//10to1//15
/ E  fb signed byte fraction in [-1,1] 1//7















/ E  fuiunsigned fraction in [0,1] 0//15to0//32
/ E  fusunsigned short fraction in [0,1] 0//10to0//16
/ E  fubunsigned byte fraction in [0,1] 0//8






Table 2.2: Storage types. A storage type, indicated with one of the given suffixes, gives both the size of data stored and how it is interpreted.

The “double-precision” storage type may actually be implemented using a single precision value on some compilation targets. GPUs currently do not support double precision, but we might want to use the same stream program and compile it to both a CPU (where double precision is no problem) and a GPU (where it is). Double precision in Sh therefore really means “as much precision as feasible”.

In other cases, a storage type may automatically be upgraded to the next available storage type if there is no cost or if a particular type is not available. For instance, if half floats are not available on a given target or would be unreasonably expensive, they will be upgraded to single-precision floats.

The integer storage formats are mostly useful for defining textures holding values of these types. Inside shaders, they will generally be represented using a combination of floats and clamping instructions, which may be more expensive than you expect and may also cause some range restrictions. This is the reason for the 10-bit precision limit on short integers and the 15-bit precision limit on integers given above. These limits are due to the simulation of integers on various platforms using the worst-case floating point precision. Using the 1/8/15-bit floating point in the fragment shader on ATI 9700 series GPUs, for instance, means that short, regular, and long integers alike will be simulated inside shader computations using only 15 bits of precision, since this is the maximum length of the significand supported by that target. On NVIDIA GeForceFX GPUs, simulation of long and regular integers with single-precision floats will lead to a 23-bit limits, and short integers (simulated using half floats) will have a limit of 10 bits.

Under no condition should you assume anything about the wraparound behaviour or sign representation of “integers”. Since on the current generation of GPUs integers must be simulated with floating point numbers, exceeding the range restrictions means that “integers” will start to lose low-order bits, not wrap around. Also, if we must simulate integers using floating point, the sign representation will be sign-magnitude, not two’s complement. But you should not assume anything either way, since future GPUs may add better support for integer types.

/ E  The fraction types are used to reflect the usual representation of colors in texture maps. In the case of an unsigned byte fraction, we would store an 8-bit integer in the texture, but divide its value by 255 when reading it into the floating-point unit to get a number in the range of [0,1] inclusive [?]. This is called a scaled-integer representation, and is usually the most efficient texture storage format to read from on GPUs. Other representations actually require us to insert shader code to transform the retreived value (for instance, to scale by 255 to get integer bytes, or subtract 0.5 and multiply by 2 to get signed fractions).

The [0,1] and [-1,1] range restrictions on fractions are not enforced inside shaders. If you really need that behaviour, use a library function to clamp the value. Sh will optimize away such functions if they are not really needed, but by default will not insert extra clamping code for fraction storage types if it is implementing them using floating-point values inside a shader.

2.2.3 Binding Types

Tuples can be used in several different situations. If they are declared outside of a shader definition, they are considered to be parameters. Such tuples can be acted upon using all the operators and functions defined in Sh, but the computations will take place on the host. In this way, Sh tuples (and matrices, and textures) can be used as a host-based “immediate mode” library. It should be noted that shaders can actually be debugged this way using a standard IDE.





Binding Types



Name Token Meaning



default SH_TEMP local parameter or temporary
Const SH_CONST definition-time constant
Input SH_INPUT input attribute
OutputSH_OUTPUToutput attribute
InOut SH_INOUT input and output attribute




Table 2.3: Binding types. A binding type indicates where a value is stored. The interpretaton of the local type (the default) depends on whether it is used inside or outside a program definition.

However, a more usual use of parameters is as global variables inside shader definitions. Nothing special is required to import a parameter into a shader: the shader definition just needs to use the parameter, and the Sh runtime will automatically track which parameters are used by what shaders, and will allocate and load constant registers as needed. Parameters, however, cannot be updated inside shaders.

If a tuple is declared inside a shader, then it is considered to be a local temporary, and is allocated to a temporary register.

Any tuple may also be declared with an Input , Output or InOut prefix, such as ShInputColor3f. These are used to declare the input and outputs of a shader, and are called attributes. The binding of inputs and outputs is both by semantic type and order and depends on the backend being used. For example, under OpenGL the first ShInputTexCoord declared is bound to GL_TEXTURE0, the second to GL_TEXTURE1, and so forth, as explained in Section 7.1.

InOut tuples internally correspond to an input and an output tuple. This is useful in many situations, e.g. if one is transforming a position between two coordinate frames, or simply passing through a value without changing it, in which case one only needs to declare an appropriate InOut variable.

Const tuples are Sh program compile-time (i.e. backend compile time) constants. It is in fact possible to assign to these types, but unlike uniform parameters the change will have no effect on previously defined programs. The Sh compiler can use this information as a hint to make a more efficient program, because it can assume that the value will never change.

2.2.4 Template Declaration

Normally we only use sizes 1, 2, 3, and 4 for tuples, and know in advance whether we want to use them for input or output. However, a template-based mechanism for declaring tuples is also supported that can be convenient in some situations. An ShAttrib tuple with five components, for instance, can be declared using ShAttrib<5>. The template declaration also has a second argument giving the binding type, which can be one of SH_TEMP , SH_CONST , SH_INPUT , SH_OUTPUT , or SH_INOUT .

These are useful in template declarations of inputs and outputs from shaders. For example, the output types of a vertex shader must match the input types of the corresponding fragment shader. Consider Listing ??, which uses a template type to make this explicit. The corresponding vertex and fragment shaders are given in Listing ?? and Listing ??.

template <ShBindingType IO>
struct BlinnPhongVertFrag 
  ShVector<3,IO> hv;         // half-vector (VCS)
  ShTexCoord<2,IO> u;        // texture coords
  ShNormal<3,IO> nv;         // normal (VCS)
  ShColor<3,IO> ec;          // irradiance
;

ShProgram blinnphong_vertex = SH_BEGIN_PROGRAM("gpu:vertex") 
  // declare input vertex parameters (unpacked in order given)
  ShInputTexCoord2f ui;      // texture coords
  ShInputNormal3f nm;        // normal vector (MCS)
  ShInputPosition3f pm;      // position (MCS)

  // declare outputs vertex parameters (packed in order given)
  BlinnPhongVertFrag<SH_OUTPUT> out;
  ShOutputPosition4f pd;     // position (HDCS)

  // specify computations
  ShPoint3f pv = (MV|pm)(0,1,2);             // VCS position
  pd = VD|pv;                                // DCS position
  out.nv = normalize((nm|VM)(0,1,2));        // normalized VCS normal
  ShVector3f lv = normalize(light.pv - pv);  // normalized VCS light vector
  out.ec = light.c * pos(nv|lv);
  ShVector3f vv = normalize(-pv);            // normalized VCS view vector
  out.hv = normalize(lv + vv);               // normalized VCS half vector
  out.u = ui;
 SH_END;

ShProgram blinnphong_fragment = SH_BEGIN_PROGRAM("gpu:fragment") 
  // declare input fragment parameters (unpacked in order given)
  BlinnPhongVertFrag<SH_INPUT> in;
  ShInputPosition3f pd;  // fragment position (DCS)

  // declare output fragment parameters (packed in order given)
  ShOutputColor3f c;             // fragment color

  // compute texture-mapped Blinn-Phong model
  c = in.ec * kd[in.u]
    + ks[in.u] * pow((normalize(in.hv)|normalize(in.nv)),q);
 SH_END;

The default binding type is SH_TEMP and is used for normal parameters (when a tuple is declared outside a shader) and for local temporaries (when a tuple is declared inside a shader).

Compile time constants have binding type SH_CONST and, as mentioned above, act like uniforms whose values are never updated, allowing certain optimizations.

Note that tuples can be of any fixed length greater than zero. The compiler will break them into 4-tuples (or smaller values) if that is what the hardware requires, but the shader writer does not have to worry about it.

Another thing the shader writer does not have to worry about is writing to inputs or reading from outputs. Both are legal in Sh even if the hardware does not support these operations; the Sh compiler just transforms the program as necessary. / E  Finally, inside shader programs, temporaries are always initialized to zero. This initialization is normally optimized away if it is not used (or if the hardware initializes the corresponding register to zero anyways).


Note: This manual is available as a bound book from AK Peters, including better formatting, in-depth examples, and about 200 pages not available on-line.