-
Notifications
You must be signed in to change notification settings - Fork 57
Intro to OWL Variables
One of the key concepts used in OWL is the concept of "variables" to pass information from the host program to device code. For example, if you want to create a triangle mesh geometry with certain material data, you'll probably want to have "something" on the device where these material parameters get stored ... and you'll need a way to set those from the host. (Where they get stored on the device is typically in the shader binding table, but OWL takes care of that, so you don't have to worry about that).
OWL uses variables to specify data for the following kind of objects:
- Geometries, and their Closest-Hit, Any-Hit, and possibly Intersect- and Bounds programs.
- RayGen programs
- Miss programs
- Launch Parameters
How to declare the variables for these different object types is sligthly different for geometries than for raygen programs, miss programs, and launch parameters, but the basic concept is the same.
Using variables - for any of these types - consists of four steps that we will outline below:
a) declaring the device-side C++/CUDA class or struct that the program(s) operate on on the device.
b) creating an OWLRayGen
, OWLMissProg
, OWLGeoType
, etc... on the host that describes this device-side struct, and what "Variables"/members this class has.
c) "setting" these objects' variables on the host (i.e., assigning values to them).
d) letting OWL care about getting these values to the device(s), putting them into shader binding table or optix launch parameters, etc...
For now, let us follow the just sketched example of a triangle mesh with simple material data. The first thing to do is to create a CUDA/C++ class that stores the device-side data - which for our example might look a bit like this:
struct TriMesh {
float3 *vertices;
float3 *normals;
int3 *indices;
struct {
float3 diffuseColor;
cudaTextureObject_t texture;
} material;
};
Second, before you can create any actual geometries of this type,
you will first have to "declare" to OWL what kind of data you're
expecting this geometry to have - basically, the size of this struct,
and the layout and types of the members. For geometries, you'd do that
by first creating a OWLGeomType
that specifies exactly this
information (using an array of OWLVarDecl
s to specify the members),
for example, as follows:
OWLVarDecl triMeshVars[] = {
{ "vertices", OWL_BUFPTR, OWL_OFFSETOF(TriMesh,vertices) },
...
{ "diffuseColor", OWL_FLOAT3, OWL_OFFSETOF(TriMesh,material.diffuseColor) },
{ "texture", OWL_TEXTURE, OWL_OFFSETOF(TriMesh,material.texture) },
{ nullptr /* sentinel to mark end of list */ }
};
Each variable in this list has a name by which it can be set on the host, a type that tells OWL what type to expect on the device side, and a offset (in bytes) within the struct.
Note:
a) OWL does come with a set of common types pre-specified (eg,
OWL_FLOAT
, OWL_INT3
, etc); most types get simply "copied" to the
device, but special rules apply to certain types like buffers and
textures, which may been some "translation" from their host-side
representation to actual device addresses. In the example above, we
have declared the float3 *vertices
member as a OWL_BUFPTR
, which
means that if it gets set as a OWLBuffer
on the host, OWL will
automatically write the proper address of the device-side data into
the struct. Similarly, a "OWL_TEXTURES" gets written as a OWLTexture
on the host, and on the device gets written as a
cudaTextureObject_t
; and a OWL_GROUP
type gets set as a OWLGroup
on the host, and written on the device as a OptixTraversableHandle
.
b) the names you assign to variables on the host do not have to exactly correspond to those on the device struct, nor do they have to be valid C/C++ identifier names; they can be any names, can even contain special characters, etc. (it is, however, highly recommended to use matching names where possible).
c) there is no type checking on what types you claim variables to
have - if, for instance, you declare a variable as OWL_FLOAT
even
though on the device it's actually a int
, then you'll get funny
results. OWL will do some type checking that what you Set
on a
variable actually matches the type you declared it to be (eg, if you
try to owlGeomSetBuffer(...)
on a variable you declared as
OWL_FLOAT
, you'll get an error); but OWL cannot know what actual
C/C++ types you use on the device.
Now that we know how to declare the members of a struct, we can use
that to declare types. Eg, we can create a OWLGeomType
for a OWL
geometry that uses the above TriMesh
struct as follows:
OWLGeomType triMeshGT = owlGeomTypeCreate(context,sizeof(TriMeshType), triMeshVars, -1);
Similarly, we could create a raygen program, or launch parameters, using, e.g.,
OWLParams launchParams = owlParamsCreate(context,sizeof(MyLaunchParams), myLaunchParamsVars, -1);
etc. A few notes:
a) you can specify the number of variables in a vardecl explicitly;
or - as i've done it in this example - end the list with a {nullptr}
sentinel and specify -1
, which is a bit less error-prone when adding
members later on.
b) the sizeof(...)
parameter above specifies the expected size of
the type on the device; for geometries, ray-gens, and miss programs
this is what OWL will reserve space for in the shader binding table,
for launch parameters it is what OWL expects to upload into the
"optixLaunchParams" variable.
Once you have created an object that does have variables (e.g., an
OWLParams
, an OWLRayGen
, or an OWLGeom
created from an
OWLGeomType
), then you can set this object's members variables using
the owl<Object>Set<...>()
helpers. E.g., an owlRayGenSet3f(...)
would
set an OWL_FLOAT3
variable on an object of type OWLRayGen
, an
owlParamsSetBuffer(...)
would set an OWL_BUFPTR
variable on an
object of type OWLParams
, etc...
Though setting variables works exactly the same for ray-gens,
geometries, params, etc... when an owl<Type>Set()
will take
effect depends on what type of object gets set: For an object of
type launch params, setting a variable will automatically take
effect the next time the respective launchParams is used in a
owlLaunch2D()
call:
owlParamsSet3f(lp,"camera.origin",camera.x,camera.y,camera.z);
...
owlLaunch2D(myRayGen, launchSize.x, launchSize.y, lp);
Unlike launch params, geometries, miss program, and ray-gen program live in the shader binding table (SBT), so variable changes to such objects will only take effect after the SBT has been rebuilt. Remember that if you have a lot of geometries building the SBT can become expensive, so try to avoid rebuilding unless required (for frequently changing variables like, for example, camera position, frame ID, frame buffer, etc, the best solution is usually to pass these through launch params rather than through the raygen program - unlike the latter, launch params do not require an SBT rebuild!).
Assuming you have properly delared the types and variables you want to
use, have properly set their variables, and have rebuilt the SBT, you
can then access the respective type's data in the respective program
on the device side using OWL's owl::getProgramDataPointer()
function:
OPTIX_CLOSEST_HIT_PROGRAM(TriMesh_ClosestHit)
{
const TriMesh *self = (const TriMesh *)owl::getProgramDataPointer();
int3 indices = self->indices[optixGetPrimitiveIndex()];
...
}
Alternatively, you can also use its slightly more convenient templated C++ version as
OPTIX_CLOSEST_HIT_PROGRAM(TriMesh_ClosestHit)
{
const TriMesh &mesh = owl::getProgramData<TriMesh>();
int3 indices = mesh.indices[optixGetPrimitiveIndex()];
...
}
Launch parameters (see below) do not live in the SBT, and will instead
appear in the global __constant__
-memory optixLaunchParams
variable... but in all other aspects will behave similarly.