Partager via


Short Vector Types in C++ AMP

You probably have heard about short vector types in C++ AMP from the introduction of the concurrency::graphics namespace, and I will also assume you have read the post on the norm and unorm scalar types.

In this post, I’m going to provide more details on short vector types, which emulate the behavior of short numerical vectors which are available in shader languages like HLSL and widely used in computer graphics programming. First I’ll talk about the concept, then about construction, then about the swizzling accessors, then about other operations, and I will end with an example of using metadata and reflection.

Short Vector Types Basics

To start using short vector types, you need to do the following:

 #include <amp_graphics.h>
 using namespace concurrency::graphics;

C++ AMP short vector types are named as: ScalarType_N, where ScalarType is one of int, uint, float, double, norm, and unorm , and N is one of 2, 3, or 4. Note that uint is a typedef of unsigned int, available in the concurrency::graphics namespace. A ScalarType_N represents a short vector of N ScalarType components. So in total, there are 18 short vector types. For example, uint_2 represents a short vector of 2 unsigned integers. Short vector types can be used in both restrict(amp) and restrict(cpu) functions.

For each short vector type, there is a corresponding typedef without the underscore, e.g. uint2, which is the syntax typically expected by graphics programmers. These typedef’s reside in the concurrency::graphics::direct3d namespace.

Creating Short Vector Types

There are multiple ways to construct a short vector type object. With default construction, all the components are initialized to 0 values. For example,

 uint_2 a;

Both components of “a” are initialized to 0. The copy construction and assignment are component-wise copy. For example,

 uint_2 b1(a); // copy construction
 uint_2 b2;
 b2 = b1; // assignment

You can also construct a ScalarType_N object by specifying the value for each of the N components via a constructor that takes N ScalarType parameters. For example,

 float_3 c(1.1f, 2.2f, 3.3f);

You can also initialize all components with the same value. For example,

 double_4 d1(3.14);

All four components of “d1” will be initialized to the double value of 3.14. This constructor also allows the implicit conversion from a ScalarType to ScalarType_N. The scalar value is copied into each component of the short vector object. For example,

One can construct a ScalarType_N object with an AnotherScalarType_N object. Each scalar component of the latter is converted to ScalarType using C++ conversion rule for primitive types or the conversions defined for norm/unorm type. The constructor is annotated with “explicit”, so the conversion from a ScalarType2_N to a ScalarType1_N has to be explicit. For example,

 float_3 e1(1.1f, 2.2f, 3.3f);
 uint_3 e2(e1); // OK, set e2 to ((uint)1.1f, (uint)2.2f, (uint)3.3f) -> (1, 2, 3)
 uint_3 e3 = e1; // Error, no implicit conversion
 uint_3 e4 = static_cast<uint_3>(e1); // OK, explicit conversion

Swizzling Accessors

Unlike index, and extent, short vector types have no notion of significance, and cannot be indexed using subscript operator. The only ways to get or set the components of a short vector are

  • construction (only set) as explained in previous section, and
  • using a swizzling format accessor.

C++ AMP short vector types offer the syntax for accessing the component(s) as

ScalarType_N_Obj.ident

ScalarType_N_Obj is a short vector of type ScalarType_N. The ident is an identifier specifying which components out of the ScalarType_N_Obj are selected for accessing. The above expression can be an l-value or an r-value. The ident is a combination of no more than N characters. The individual character in ident may be x, y, z, w, r, g, b, and a. Note that x, y, z, and w means the first, the second, the third, and the fourth component of the short vector respectively. As you may have guessed, r, g, b, a are synonymous for x, y, z, w. In ident, characters from group x, y, z, w, cannot be combined with characters from group r, g, b, a. Single component accessor, such as x or y, is type of ScalarType. Multi-component accessor is type of ScalarType_M, where M is the number of components in ident. For example, the type of e3.xy is uint_2.

The ident can use swizzling format to rearrange the components from ScalarType_N_Obj. For example, one can write,

 double_4 f1(3.14);
 double_4 f2 = f1.xyzw;  // no rearrangement
 double_4 f3 = f1.zwxy;  // rearrangement
 f3.zxwy = f1.xwyz; // rearrangement

The above code is equivalent to

 double_4 f2 = f1; 
 double_4 f3(f1.z, f1.w, f1.x, f1.y); 
 f3.z = f1.x; f3.x = f1.w; f3.w = f1.y; f3.y = f1.z;

For the initial release, C++ AMP does not offer all the possible swizzling formats. For a ScalarType_N, the swizzling format can have 1 to N components, each from the character group x, y, z, and w, and in any order. Repetition of a character is not allowed. The same set of swizzling formats are available using the character group r, g, b, and a. Below are examples of supported usages:

 double_4 g1(3.14);
 double g2 = g1.y;
 double_2 g3 = g1.wx;
 double_3 g4 = g1.ywx;
 g4.rbg = g1.xwy;
 g4.r = g1.z;
 g4.xy = g1.ba;

Here are some unsupported usages:

 double_2 h1 = g1.xx; // repetition of x
 double_3 h2 = g1.xyr; // r could not be combined with xy
 h1.xyz = g1.xy; // h1 can only have up to 2 components

For unsupported swizzling format, you will have to explicitly use constructors.

 double_2 i1 = double_2(g1.x, g1.x); // to achieve g1.xx

With Visual Studio 2012, you can always use Intellisense to discover the available usages of swizzling formats, for example:

intellisense

Operations

A variety of operations are supported for short vector types. Binary operators, compound assignment operators, and relational operators, are defined between two short vector types with the same ScalarType and N. Below are the supported operators:

  • Arithmetic operators: +, -, *, /
  • Bitwise operators (only for int_N and uint_N): %, ^, |, &, <<, >>, and ~
  • Compound assignment operators:
    • arithmetic operations: +=, -=, *=, /=
    • bitwise operations: only for int_N, and uint_N: %=, ^=, |=, &=, <<=, >>=
  • Equality operators: ==, !=
  • Increment and decrement operators: ++, -- both prefix and postfix
  • Unary “-“ operator (Not available for unorm_N, and uint_N)

For all above operators, the operation is carried component-wise except for equality/inequality operators. For equality operator, “==”, it returns true if all components are equal. For inequality operator, “!=”, it returns true if any pair of components are not equal. Here are some examples:

 uint_2 j1(1U, 2U), j2(3U, 4U); // j1 : (1, 2); j2 : (3, 4)
 uint_2 j3 = j1.yx + j2; // j3 : (5, 5)
 j3 += j2; // j3 : (8, 9)
 j3--;     // j3 : (7, 8)
 uint_2 j4 = ~j3;    // j4 : (fffffff8, fffffff7) 
 int_3 j5(1, 2, 3);    // j5 : (1, 2, 3)
 int_3 j6 = -j5;    // j6 : (-1, -2, -3)
 bool j7 = (j5 == j6); // j7 : false

For any operation that is defined between two short vectors, the same operation can be performed between a short vector and a scalar. The scalar’s type should either be the same as the short vector’s ScalarType, or be a type that could be implicitly converted to ScalarType_N with only one user-defined conversion. The operation is carried component-wise between each component of the short vector and the scalar. Here are some examples:

 uint_2 k1(1U, 2U);  // k1 : (1, 2)
 uint_2 k2 = k1 + 3U; // k2 : (4, 5)
 uint_2 k3 = 4U + k1; // k3 : (5, 6) 
 k3 += 5U; // k3 : (10, 11)
 int k4 = 6;
 k3 += k4;  // int -> uint -> uint_2, 
            // with only one user defined conversion uint -> uint_2
 // k3 : (16, 17)
 bool k5 = (k3 != 1);    // k5: true;

Metadata and Reflection

C++ AMP also provides some C++ metaprogramming facilities for short vector types.

Every short vector type exposes its number of components as a static member called size of type int, e.g., double_4 contains the following:

 static const int size = 4;

Every short vector type exposes its scalar type as a typedef, consistent with STL containers, e.g., double_4 has:

 typedef double value_type;

Another generic way to retrieve the size and value_type of a short vector type is to use a class called short_vector_traits, which allows you to write code like:

 short_vector_traits<double_4>::size // return 4
 short_vector_traits<double_4>::value_type // a typedef resolving to “double”

short_vector_traits also support scalar types, for example:

 short_vector_traits<double>::size // return 1
 short_vector_traits<double>::value_type // a typedef resolving to “double”

We also provide a class called short_vector, which is useful for programming short vectors generically, as

 template<typename ScalarType, int N> 
 struct short_vector;

short_vector only has a typedef member called type. For the six ScalarType’s, when N is 1, short_vector::type is ScalarType; when N is 2, 3, or 4, short_vector::type is ScalarType_N. For all other instantiation of short_vector, a static assertion will be triggered. For example,

 short_vector<int, 1>::type a; // int
 short_vector<int, 2>::type b; // int_2
 short_vector<int, 5>::type c; // static assertion

Following is an example using the above reflection classes. In this release, we don’t provide a stream operator for short vector types. However, you can implement it using short_vector, and short_vector_traits.

 template <typename ScalarType, int N> 
 class stream_helper;
  
 // specialization for N == 2
 // ...
  
 // specialization for N == 3
 template<typename ScalarType> 
 class stream_helper<ScalarType, 3>
 {
 public:
     typedef typename short_vector<ScalarType, 3>::type short_vector_type;
     static std::ostream& stream(std::ostream &os, const short_vector_type& svt)
     {
         os << "(" << svt.x << ", " << svt.y << ", " << svt.z << ")";
         return os;
     }
 };
  
 // specialization for N == 4
 // ...
  
 template <typename ShortVectorType>
 inline
 typename std::enable_if<(short_vector_traits<ShortVectorType>::size > 1), 
                         std::ostream&>::type
 operator<<(std::ostream &os, const ShortVectorType& svt)
 { 
     return stream_helper<typename short_vector_traits<ShortVectorType>::value_type, 
                          short_vector_traits<ShortVectorType>::size>::
            stream(os, svt); 
 }

Then you can write code like,

 int_3 a(1, 2, 3);
 float_3 b(4.1f, 5.2f, 6.5f);
 std::cout << a << std::endl;
 std::cout << b << std::endl;

It outputs:

 (1, 2, 3)
 (4.1, 5.2, 6.5)

This concludes my introduction to short vector types. As always, your feedback is welcome below or in our MSDN Forum.