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

93 views Asked by At

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?

1

There are 1 answers

0
Ingo Wald On BEST ANSWER

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.