c++raytracingembree

Embree: stream mode - how does gather and scatter work and what are pid and tid?


I'm trying to upgrade my application from single ray intersection to stream intersection.

What I don't quite understand is how it's possible that the gather and scatter functions shown in the tutorials are even working

The example defines a custom extended ray struct Ray2

struct Ray2
{
  Ray ray;

  // ray extensions
  float transparency; //!< accumulated transparency value

  // we remember up to 16 hits to ignore duplicate hits
  unsigned int firstHit, lastHit;
  unsigned int hit_geomIDs[HIT_LIST_LENGTH];
  unsigned int hit_primIDs[HIT_LIST_LENGTH];
};

then it defines an array of these Ray2structs:

Ray2 primary_stream[TILE_SIZE_X*TILE_SIZE_Y];

this array is set as the userRayExt before calling the intersection method:

primary_context.userRayExt = &primary_stream;
rtcIntersect1M(data.g_scene,&primary_context.context,(RTCRayHit*)&primary_stream,N,sizeof(Ray2));

now, for each ray bundle that embree intersects with geometry, the filter callback is invoked:

/* intersection filter function for streams of general packets */
void intersectionFilterN(const RTCFilterFunctionNArguments* args)
{
  int* valid = args->valid;
  const IntersectContext* context = (const IntersectContext*) args->context;
  struct RTCRayHitN* rayN = (struct RTCRayHitN*)args->ray;
  //struct RTCHitN* hitN = args->hit;
  const unsigned int N = args->N;
                                  
  /* avoid crashing when debug visualizations are used */
  if (context == nullptr) return;

  /* iterate over all rays in ray packet */
  for (unsigned int ui=0; ui<N; ui+=1)
  {
    /* calculate loop and execution mask */
    unsigned int vi = ui+0;
    if (vi>=N) continue;

    /* ignore inactive rays */
    if (valid[vi] != -1) continue;

    /* read ray/hit from ray structure */
    RTCRayHit rtc_ray = rtcGetRayHitFromRayHitN(rayN,N,ui);
    Ray* ray = (Ray*)&rtc_ray;

    /* calculate transparency */
    Vec3fa h = ray->org + ray->dir  * ray->tfar;
    float T = transparencyFunction(h);

    /* ignore hit if completely transparent */
    if (T >= 1.0f) 
      valid[vi] = 0;
    /* otherwise accept hit and remember transparency */
    else
    {
      /* decode ray IDs */
      const unsigned int pid = ray->id / 1;
      const unsigned int rid = ray->id % 1;
      Ray2* ray2 = (Ray2*) context->userRayExt;
      assert(ray2);
      scatter(ray2->transparency,sizeof(Ray2),pid,rid,T);
    }
  }
}

the last line of this method is what I don't understand

scatter(ray2->transparency,sizeof(Ray2),pid,rid,T);

I understand what it is SUPPOSED to do. It should update the transparency property of the Ray2 that corresponds to the traced ray with T. But I don't get why/how this works, since the implementation of scatter looks like this:

inline void scatter(float& ptr, const unsigned int stride, const unsigned int pid, const unsigned int rid, float v) {
  ((float*)(((char*)&ptr) + pid*stride))[rid] = v;
}

I will reformulate this function a bit to better ask my question (but it should be completely equivalent if I'm not mistaken):

inline void scatter(float& ptr, const unsigned int stride, const unsigned int pid, const unsigned int rid, float v) {
  float* uptr = ((float*)(((char*)&ptr) + pid*stride));
  uptr[rid] = v;
}

So, the first line still makes sense for me. A pointer to the transparency field of the first Ray2 struct is constructed and then incremented by tid * sizeof(Ray2) - this makes sense as it will land on another transparency field, since it is incremented by a multiple of sizeof(Ray2)

but then the next line

uptr[rid] = v;

I don't get at all. uptr is a float pointer, pointing to a transparency field. So unless rid itself is a multiple of sizeof(Ray2), this won't point to a transparency field of one of the rays at all.

pid and rid are calculated as

  const unsigned int pid = ray->id / 1;
  const unsigned int rid = ray->id % 1;

which I find weird. Isn't that always the same as

  const unsigned int pid = ray->id;
  const unsigned int rid = 0;

?

what are pid and rid and why are they computed like this?


Solution

  • Having not written this example myself it's hard to guess what the original intention of it was, but I think the clue lies in exactly your observation that for rid and pid calculations, the division/modulo by '1' are meaningless.

    So, if rid eventially always ends up as being '0' (because every value mod 1 will be 0 :-/), then uptr[rid] = ... is equivalent to *uptr = ..., which is in fact correct since you yourself pointed out that uptr always points to a valid transparency.

    Now as to why the code does this confusing pid/rid thing? If I had to guess from the naming of "Ray2" I would assume that a different version of this sample maybe used two rays and two transparencies in that ray2 struct, and then used the rid/pid thing to always select the right one of the pair.

    Still, as to the original question of "why does this work at all" : rid always evaluates to 0, so it does always write right into the transparency value that uptr points to.