Skip to content
Ingo Wald edited this page Nov 6, 2020 · 6 revisions

Welcome to the OWL Wiki - this page for now is mostly to explain how different concepts in OWL work, usually based on questions users had.

Variables

One of the key concepts used in OWL is the concept of "variables" to pass information from the host program to the 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 etc 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, BTW, but OWL takes care of that, so you don't have to worry about that).

Declaring the actual C/C++ Types

For now, let's follow this simple example of a triangle mesh with material data. Then, the first thing to do is 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;
};

Declaring the set of Variables used in the C/C++ Type

Second, before you can create any actual geometries of this type, you'll 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 OWLVarDecls 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.

Declaring a Type that uses Variables

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.

Setting Variables on the Host

Once you have created an object that does have variables (eg, a OWLParams, a OWLRayGen, or a OWLGeomcreated from aOWLGeomType), then you can set this object's members variables using the owlSet<...>()helpers. Eg, aowlRayGenSet3f(...)would set aOWL_FLOAT3variable on a object of typeOWLRayGen, a owlParamsSetBuffer(...)would set aOWL_BUFPTRvariable on a object of typeOWLParams`, etc.

Tough setting variables works exactly the same for ray-gens, geometries, params, etc, always works exactly the same way, when a owl<Type>Set() will take effect depend 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!).

Accessing Variable Data on the Device

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.

Launch Parameters

Before going into how launch params works in OWL, it's important to first understand how they work in OptiX

The Concept of Launch Parameters in OptiX

Launch parameters - in OptiX speak - are a way of specifying GPU "constant memory" data for a launch that is globally visible to all the programs used in the launch. Eg, let's assume you have the following skeleton code in your device code:

struct MyLaunchParams {
   int frameID;
   float4 *frameBuffer;
   OptixTraversableHandle worldIAS;
};
__constant__ MyLaunchParams optixLaunchParams;

OPTIX_RAYGEN_PROGRAM(renderFrame)() { 
   .... optixLaunchParams.frameBuffer[...] = .... ;
}

OPTIX_CLOSEST_HIT_PROGRAM(TriMeshCH)() { ... }

In this example, the optixLaunchParams will be globally visible to any of the programs that may get executed during a launch (i.e., both raygen and CH program will see it).

The value of this optixLaunchParams is constant for the duration of one launch, but can take different values for different launches; in particular, changing the launch params between two launches does not require rebuilding the SBT or recompiling any programs, so is very(!) lightweight way of specifying frequently changing data like camera position, frame buffer pointers, random number seeds, etc.

In fact, launch params can even contain different values for different launches that are running asynchronously, and/or in parallel ... they're awesome.

Launch Params in OWL

Launch params are often overlooked by novices, maybe because they're optional (you don't have to use any at al if you don't want to!), and/or because it may seem more natural to pass parameters to a raygen program in the same way one'd pass them to closest-hit or any-hit programs (ie, through the SBT). They do, however, have a lot of very useful properties: e.g, they can be changed without rebuilding the SBT, they can have different values during async launches, they live in constant memory (instead of global memory for SBT data), they are globally visible in all programs (without wasting registers for passing pointers to shared data), etc.

Consequently, I personally now pass virtually every raygen or miss program variable through launch params; and launch params have become "prime citizens" in OWL. Let's first start with a simple example.

Assuming you have a few variables you want to be globally accessible to all programs in your pipeline/context, and/or that you want to change frequently per frame. If so, you'd first put those into a struct, and declare a global constant-memory variable named optixLaunchParams, e.g., like that:

struct MyGlobals {
   Camera  camera;
   int     frameID;
   float4 *frameBuffer;
   OptixTraversableHandle world;
};
__constant__ MyGlobals optixLaunchParams;

You can then access this optixLaunchParams in every program.

Important: though you can declare the struct any way you want, the constant-memory variable that holds these launch parameters has to be called optixLaunchParams - you can assign different values for different launches, but the name cannot be changed.

Creating, Setting, and Using Launch Params (LPs) on the Host

Once that struct has been defined, you can declare its variables (see above), and create an object of type OWLParams as follows:

OWLVarDecl myGlobalsVars[] = { .... see 'variables' section above ... };
OWLParams  lp = owlParamsCreate(context,sizeof(MyGlobals),myGLobalsVars,-1);

... and can then set its variables like for any other object that can take variables:

owlParamsSet1i(lp,"frameID",frameID++);
owlParamsSet3f(lp,"camera.origin", ....);
....

Once all of the variables required for the next launch have been set, you can then launch a raygen program with the given LPs through owlLaunch2D():

owlLaunch2D(myRayGen, launchSize.x, launchSize.y, lp);

Upon encountering this call, OWL will automatically gather and upload the required variables (including any potential type translations for group, texture, or buffer variables, see above), will copy them into the specified constant-memory variable named optixLaunchParams, and will then execute the given raygen program (with given launch dimensions). By default it will then wait for that launch to complete, but this can be changed, as I'll describe in a second.

LPs can be changed between launches; and you'll obviously have to change only those variables that you do want to change; variables, once set, will retain their values across multiple launches, until they get explicitly assigned a new value.

Clone this wiki locally