spu_tri.c:flush_spans() contained a big blob of conditional code
that would check DMA completion on almost every call to that
frequently called function, so my goal has been to move that check
up and out.

To achieve this, I have implemented double-buffering of DMA of color
and z tiles.

The use of DMA tags has been reduced, there is now one for each tile
buffer, regardless of use - that is, there is no longer separate
read, write or clear tags.  By allocating tags in a per-buffer
fashion, it is possible to use a DMA fence to control ordering,
which has made it possible to remove some other explicit
synchronisation calls.

A long-standing image corruption bug has been fixed - tiles were
being cleared before DMA put operations had completed.

The main loop in spu_render.c:cmd_render() has been heavily modified
to be able to pre-calculate tile numbers to facilitate double buffering.

Comments?

jonathan.
diff --git a/src/gallium/drivers/cell/spu/spu_main.h 
b/src/gallium/drivers/cell/spu/spu_main.h
index 33767e7..ff21180 100644
--- a/src/gallium/drivers/cell/spu/spu_main.h
+++ b/src/gallium/drivers/cell/spu/spu_main.h
@@ -155,8 +155,10 @@ struct spu_global
    struct vertex_info vertex_info;
 
    /** Current color and Z tiles */
-   tile_t ctile ALIGN16_ATTRIB;
-   tile_t ztile ALIGN16_ATTRIB;
+   tile_t ctile[2] ALIGN16_ATTRIB;
+   tile_t ztile[2] ALIGN16_ATTRIB;
+
+   int buffer;
 
    /** Read depth/stencil tiles? */
    boolean read_depth_stencil;
@@ -196,12 +198,11 @@ extern struct spu_global spu;
 
 /* DMA TAGS */
 
-#define TAG_SURFACE_CLEAR     10
 #define TAG_VERTEX_BUFFER     11
-#define TAG_READ_TILE_COLOR   12
-#define TAG_READ_TILE_Z       13
-#define TAG_WRITE_TILE_COLOR  14
-#define TAG_WRITE_TILE_Z      15
+#define TAG_TILE_COLOR        12
+#define TAG_TILE_COLOR_DBL    13  // Not used by name, but by offset from 
TAG_TILE_COLOR
+#define TAG_TILE_Z            14
+#define TAG_TILE_Z_DBL        15  // Not used by name, but by offset from 
TAG_TILE_Z
 #define TAG_INDEX_BUFFER      16
 #define TAG_BATCH_BUFFER      17
 #define TAG_MISC              18
diff --git a/src/gallium/drivers/cell/spu/spu_render.c 
b/src/gallium/drivers/cell/spu/spu_render.c
index 8eb3c43..6a322a7 100644
--- a/src/gallium/drivers/cell/spu/spu_render.c
+++ b/src/gallium/drivers/cell/spu/spu_render.c
@@ -45,8 +45,7 @@
  */
 static INLINE void
 tile_bounding_box(const struct cell_command_render *render,
-                  uint *txmin, uint *tymin,
-                  uint *box_num_tiles, uint *box_width_tiles)
+                  uint *txmin, uint *tymin, uint *txmax, uint *tymax)
 {
 #if 0
    /* Debug: full-window bounding box */
@@ -60,19 +59,14 @@ tile_bounding_box(const struct cell_command_render *render,
    (void) txmax;
    (void) tymax;
 #else
-   uint txmax, tymax, box_height_tiles;
-
    *txmin = (uint) render->xmin / TILE_SIZE;
    *tymin = (uint) render->ymin / TILE_SIZE;
-   txmax = (uint) render->xmax / TILE_SIZE;
-   tymax = (uint) render->ymax / TILE_SIZE;
-   if (txmax >= spu.fb.width_tiles)
-      txmax = spu.fb.width_tiles-1;
-   if (tymax >= spu.fb.height_tiles)
-      tymax = spu.fb.height_tiles-1;
-   *box_width_tiles = txmax - *txmin + 1;
-   box_height_tiles = tymax - *tymin + 1;
-   *box_num_tiles = *box_width_tiles * box_height_tiles;
+   *txmax = (uint) render->xmax / TILE_SIZE;
+   *tymax = (uint) render->ymax / TILE_SIZE;
+   if (*txmax >= spu.fb.width_tiles)
+      *txmax = spu.fb.width_tiles-1;
+   if (*tymax >= spu.fb.height_tiles)
+      *tymax = spu.fb.height_tiles-1;
 #endif
 #if 0
    printf("SPU %u: bounds: %g, %g  ...  %g, %g\n", spu.init.id,
@@ -85,32 +79,24 @@ tile_bounding_box(const struct cell_command_render *render,
 }
 
 
-/** Check if the tile at (tx,ty) belongs to this SPU */
-static INLINE boolean
-my_tile(uint tx, uint ty)
-{
-   return (spu.fb.width_tiles * ty + tx) % spu.init.num_spus == spu.init.id;
-}
-
-
 /**
  * Start fetching non-clear color/Z tiles from main memory
  */
 static INLINE void
-get_cz_tiles(uint tx, uint ty)
+get_cz_tiles_f(uint tx, uint ty, int buffer, ubyte *ctile_status, ubyte 
*ztile_status)
 {
    if (spu.read_depth_stencil) {
-      if (spu.cur_ztile_status != TILE_STATUS_CLEAR) {
-         //printf("SPU %u: getting Z tile %u, %u\n", spu.init.id, tx, ty);
-         get_tile(tx, ty, &spu.ztile, TAG_READ_TILE_Z, 1);
-         spu.cur_ztile_status = TILE_STATUS_GETTING;
+      if (*ztile_status != TILE_STATUS_CLEAR) {
+//         printf("SPU %u: getting Z tile %u, %u\n", spu.init.id, tx, ty);
+         get_tile_f(tx, ty, &spu.ztile[buffer], TAG_TILE_Z + buffer, 1);
+         *ztile_status = TILE_STATUS_GETTING;
       }
    }
 
-   if (spu.cur_ctile_status != TILE_STATUS_CLEAR) {
-      //printf("SPU %u: getting C tile %u, %u\n", spu.init.id, tx, ty);
-      get_tile(tx, ty, &spu.ctile, TAG_READ_TILE_COLOR, 0);
-      spu.cur_ctile_status = TILE_STATUS_GETTING;
+   if (*ctile_status != TILE_STATUS_CLEAR) {
+//      printf("SPU %u: getting C tile %u, %u\n", spu.init.id, tx, ty);
+      get_tile_f(tx, ty, &spu.ctile[buffer], TAG_TILE_COLOR + buffer, 0);
+      *ctile_status = TILE_STATUS_GETTING;
    }
 }
 
@@ -119,12 +105,12 @@ get_cz_tiles(uint tx, uint ty)
  * Start putting dirty color/Z tiles back to main memory
  */
 static INLINE void
-put_cz_tiles(uint tx, uint ty)
+put_cz_tiles(uint tx, uint ty, int buffer)
 {
    if (spu.cur_ztile_status == TILE_STATUS_DIRTY) {
       /* tile was modified and needs to be written back */
       //printf("SPU %u: put dirty Z tile %u, %u\n", spu.init.id, tx, ty);
-      put_tile(tx, ty, &spu.ztile, TAG_WRITE_TILE_Z, 1);
+      put_tile(tx, ty, &spu.ztile[buffer], TAG_TILE_Z + buffer, 1);
       spu.cur_ztile_status = TILE_STATUS_DEFINED;
    }
    else if (spu.cur_ztile_status == TILE_STATUS_GETTING) {
@@ -136,7 +122,7 @@ put_cz_tiles(uint tx, uint ty)
    if (spu.cur_ctile_status == TILE_STATUS_DIRTY) {
       /* tile was modified and needs to be written back */
       //printf("SPU %u: put dirty C tile %u, %u\n", spu.init.id, tx, ty);
-      put_tile(tx, ty, &spu.ctile, TAG_WRITE_TILE_COLOR, 0);
+      put_tile(tx, ty, &spu.ctile[buffer], TAG_TILE_COLOR + buffer, 0);
       spu.cur_ctile_status = TILE_STATUS_DEFINED;
    }
    else if (spu.cur_ctile_status == TILE_STATUS_GETTING) {
@@ -148,14 +134,68 @@ put_cz_tiles(uint tx, uint ty)
 
 
 /**
- * Wait for 'put' of color/z tiles to complete.
+ * Explicity wait for completion of transfers in buffer specified
  */
 static INLINE void
-wait_put_cz_tiles(void)
+wait_cz_tiles(int buffer)
 {
-   wait_on_mask(1 << TAG_WRITE_TILE_COLOR);
+   if (spu.cur_ctile_status == TILE_STATUS_GETTING) {
+      wait_on_mask(1 << (TAG_TILE_COLOR+buffer));
+      spu.cur_ctile_status = TILE_STATUS_CLEAN;
+   }
+   else if (spu.cur_ctile_status == TILE_STATUS_CLEAR) {
+      wait_on_mask(1 << (TAG_TILE_COLOR+buffer));
+      clear_c_tile(&spu.ctile[buffer]);
+      spu.cur_ctile_status = TILE_STATUS_DIRTY;
+   }
+   ASSERT(spu.cur_ctile_status != TILE_STATUS_DEFINED);
+
    if (spu.read_depth_stencil) {
-      wait_on_mask(1 << TAG_WRITE_TILE_Z);
+      if (spu.cur_ztile_status == TILE_STATUS_GETTING) {
+         wait_on_mask(1 << (TAG_TILE_Z+buffer));
+         spu.cur_ztile_status = TILE_STATUS_CLEAN;
+      }
+      else if (spu.cur_ztile_status == TILE_STATUS_CLEAR) {
+         wait_on_mask(1 << (TAG_TILE_Z+buffer));
+         clear_z_tile(&spu.ztile[buffer]);
+         spu.cur_ztile_status = TILE_STATUS_DIRTY;
+      }
+      ASSERT(spu.cur_ztile_status != TILE_STATUS_DEFINED);
+   }
+}
+
+/**
+ * Calculate the x tile number of the first tile to be rendered by this spu on
+ * tile line ty
+ */
+static uint
+calc_tx_begin(uint txmin, uint ty)
+{
+   uint ybasis = ty * spu.fb.width_tiles;
+   uint basis = ybasis + txmin;
+   uint offset = basis % spu.init.num_spus;
+   uint tx = txmin + spu.init.id - offset;
+   if (offset > spu.init.id) tx += spu.init.num_spus;
+   return tx;
+}
+
+
+/**
+ * Calculate the next tile to be drawn by this spu and return its position in
+ * tx and ty
+ */
+static void
+calc_next_ty_tx(uint * __restrict ty, uint * __restrict tx, uint tystart,
+                uint tymax, uint txmin, uint txmax, uint txfirst)
+{
+   int found = false;
+   for (*ty = tystart; *ty <= tymax;) {
+      for (*tx = txfirst; *tx <= txmax; *tx += spu.init.num_spus) {
+         found = true;
+         break;
+      }
+      if (found) break;
+      txfirst = calc_tx_begin(txmin, ++*ty);
    }
 }
 
@@ -175,9 +215,33 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
    uint index_bytes;
    const ubyte *vertices;
    const ushort *indexes;
-   uint i, j;
+   uint tx, ty, j;
    uint num_tiles;
 
+   /**
+    ** find tiles which intersect the prim bounding box
+    **/
+   uint txmin, tymin, txmax, tymax;
+   tile_bounding_box(render, &txmin, &tymin, &txmax, &tymax);
+
+   // Keep track of the tile buffer we're using
+   spu.buffer = 0;
+
+   int tiles_pending = true;
+
+   // Find the next tile to be handled by this spu
+   calc_next_ty_tx(&ty, &tx, tymin, tymax, txmin, txmax, calc_tx_begin(txmin, 
tymin));
+
+   if (ty > tymax) {
+      // No tiles will be handled by this spu
+      tiles_pending = false;
+   } else {
+     // issue fetch for first tile...
+      spu.cur_ctile_status = spu.ctile_status[ty][tx];
+      spu.cur_ztile_status = spu.ztile_status[ty][tx];
+      get_cz_tiles_f(tx, ty, spu.buffer, &spu.cur_ctile_status, 
&spu.cur_ztile_status);
+   }
+
    D_PRINTF(CELL_DEBUG_CMD,
             "RENDER prim=%u num_vert=%u num_ind=%u inline_vert=%u\n",
             render->prim_type,
@@ -231,41 +295,18 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
       wait_on_mask(1 << TAG_VERTEX_BUFFER);
    }
 
-
-   /**
-    ** find tiles which intersect the prim bounding box
-    **/
-   uint txmin, tymin, box_width_tiles, box_num_tiles;
-   tile_bounding_box(render, &txmin, &tymin,
-                     &box_num_tiles, &box_width_tiles);
-
-
-   /* make sure any pending clears have completed */
-   wait_on_mask(1 << TAG_SURFACE_CLEAR); /* XXX temporary */
-
-
    num_tiles = 0;
 
    /**
     ** loop over tiles, rendering tris
     **/
-   for (i = 0; i < box_num_tiles; i++) {
-      const uint tx = txmin + i % box_width_tiles;
-      const uint ty = tymin + i / box_width_tiles;
+   while (tiles_pending) {
 
       ASSERT(tx < spu.fb.width_tiles);
       ASSERT(ty < spu.fb.height_tiles);
 
-      if (!my_tile(tx, ty))
-         continue;
-
       num_tiles++;
 
-      spu.cur_ctile_status = spu.ctile_status[ty][tx];
-      spu.cur_ztile_status = spu.ztile_status[ty][tx];
-
-      get_cz_tiles(tx, ty);
-
       uint drawn = 0;
 
       const qword vertex_sizes = (qword)spu_splats(vertex_size);
@@ -275,10 +316,27 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
 
       const uint num_indexes = render->num_indexes;
 
+      // Calculate the location of the next tile for this spu- if there is one
+      uint txnext, tynext;
+      ubyte next_ctile_status = 0, next_ztile_status = 0;
+
+      calc_next_ty_tx(&tynext, &txnext, ty, tymax, txmin, txmax, tx + 
spu.init.num_spus);
+
+      if( tynext > tymax ) {
+         tiles_pending = false;
+      } else {
+         next_ctile_status = spu.ctile_status[tynext][txnext];
+         next_ztile_status = spu.ztile_status[tynext][txnext];
+         get_cz_tiles_f(txnext, tynext, spu.buffer^1, &next_ctile_status, 
&next_ztile_status);
+      }
+
+      // Block for completion of the current tile
+      wait_cz_tiles(spu.buffer);
+
       /* loop over tris
-          * &indexes[0] will be 16 byte aligned.  This loop is heavily unrolled
-          * avoiding variable rotates when extracting vertex indices.
-          */
+       * &indexes[0] will be 16 byte aligned.  This loop is heavily unrolled
+       * avoiding variable rotates when extracting vertex indices.
+       */
       for (j = 0; j < num_indexes; j += 24) {
          /* Load three vectors, containing 24 ushort indices */
          const qword* lower_qword = (qword*)&indexes[j];
@@ -287,7 +345,7 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
          const qword indices2 = lower_qword[2];
 
          /* stores three indices for each tri n in slots 0, 1 and 2 of vsn */
-                /* Straightforward rotates for these */
+         /* Straightforward rotates for these */
          qword vs0 = indices0;
          qword vs1 = si_shlqbyi(indices0, 6);
          qword vs3 = si_shlqbyi(indices1, 2);
@@ -296,7 +354,7 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
          qword vs7 = si_shlqbyi(indices2, 10);
 
          /* For tri 2 and 5, the three indices are split across two machine
-                 * words - rotate and combine */
+          * words - rotate and combine */
          const qword tmp2a = si_shlqbyi(indices0, 12);
          const qword tmp2b = si_rotqmbyi(indices1, 12|16);
          qword vs2 = si_selb(tmp2a, tmp2b, si_fsmbi(0x0c00));
@@ -326,7 +384,7 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
          vs7 = si_mpya(vs7, vertex_sizes, verticess);
 
          /* Select the appropriate call based on the number of vertices 
-                 * remaining */
+          * remaining */
          switch(num_indexes - j) {
             default: drawn += tri_draw(vs7, tx, ty);
             case 21: drawn += tri_draw(vs6, tx, ty);
@@ -342,13 +400,18 @@ cmd_render(const struct cell_command_render *render, uint 
*pos_incr)
       //printf("SPU %u: drew %u of %u\n", spu.init.id, drawn, 
render->num_indexes/3);
 
       /* write color/z tiles back to main framebuffer, if dirtied */
-      put_cz_tiles(tx, ty);
-
-      wait_put_cz_tiles(); /* XXX seems unnecessary... */
+      put_cz_tiles(tx, ty, spu.buffer);
 
       spu.ctile_status[ty][tx] = spu.cur_ctile_status;
       spu.ztile_status[ty][tx] = spu.cur_ztile_status;
-   }
+
+      // Set up for next iteration.
+      spu.buffer ^= 1;
+      tx = txnext;
+      ty = tynext;
+      spu.cur_ctile_status = next_ctile_status;
+      spu.cur_ztile_status = next_ztile_status;
+   };
 
    D_PRINTF(CELL_DEBUG_CMD,
             "RENDER done (%u tiles hit)\n",
diff --git a/src/gallium/drivers/cell/spu/spu_tile.c 
b/src/gallium/drivers/cell/spu/spu_tile.c
index 6905015..63c35be 100644
--- a/src/gallium/drivers/cell/spu/spu_tile.c
+++ b/src/gallium/drivers/cell/spu/spu_tile.c
@@ -35,7 +35,7 @@
  * Get tile of color or Z values from main memory, put into SPU memory.
  */
 void
-get_tile(uint tx, uint ty, tile_t *tile, int tag, int zBuf)
+get_tile_f(uint tx, uint ty, tile_t *tile, int tag, int zBuf)
 {
    const uint offset = ty * spu.fb.width_tiles + tx;
    const uint bytesPerTile = TILE_SIZE * TILE_SIZE * (zBuf ? spu.fb.zsize : 4);
@@ -47,10 +47,10 @@ get_tile(uint tx, uint ty, tile_t *tile, int tag, int zBuf)
    ASSERT(ty < spu.fb.height_tiles);
    ASSERT_ALIGN16(tile);
    /*
-   printf("get_tile:  dest: %p  src: 0x%x  size: %d\n",
-          tile, (unsigned int) src, bytesPerTile);
+   printf("get_tile_f:  dest: %p  src: 0x%x  size: %d tag: %d\n",
+          tile, (unsigned int) src, bytesPerTile, tag);
    */
-   mfc_get(tile->ui,  /* dest in local memory */
+   mfc_getf(tile->ui,  /* dest in local memory */
            (unsigned int) src, /* src in main memory */
            bytesPerTile,
            tag,
@@ -75,9 +75,9 @@ put_tile(uint tx, uint ty, const tile_t *tile, int tag, int 
zBuf)
    ASSERT(ty < spu.fb.height_tiles);
    ASSERT_ALIGN16(tile);
    /*
-   printf("SPU %u: put_tile:  src: %p  dst: 0x%x  size: %d\n",
+   printf("SPU %u: put_tile:  src: %p  dst: 0x%x  size: %d tag: %d\n",
           spu.init.id,
-          tile, (unsigned int) dst, bytesPerTile);
+          tile, (unsigned int) dst, bytesPerTile, tag);
    */
    mfc_put((void *) tile->ui,  /* src in local memory */
            (unsigned int) dst,  /* dst in main memory */
@@ -99,24 +99,26 @@ really_clear_tiles(uint surfaceIndex)
    uint i;
 
    if (surfaceIndex == 0) {
-      clear_c_tile(&spu.ctile);
+      wait_on_mask(1<<TAG_TILE_COLOR);
+         clear_c_tile(&spu.ctile[0]);
 
       for (i = spu.init.id; i < num_tiles; i += spu.init.num_spus) {
          uint tx = i % spu.fb.width_tiles;
          uint ty = i / spu.fb.width_tiles;
          if (spu.ctile_status[ty][tx] == TILE_STATUS_CLEAR) {
-            put_tile(tx, ty, &spu.ctile, TAG_SURFACE_CLEAR, 0);
+            put_tile(tx, ty, &spu.ctile[0], TAG_TILE_COLOR, 0);
          }
       }
    }
    else {
-      clear_z_tile(&spu.ztile);
+      wait_on_mask(1<<TAG_TILE_Z);
+      clear_z_tile(&spu.ztile[0]);
 
       for (i = spu.init.id; i < num_tiles; i += spu.init.num_spus) {
          uint tx = i % spu.fb.width_tiles;
          uint ty = i / spu.fb.width_tiles;
          if (spu.ztile_status[ty][tx] == TILE_STATUS_CLEAR)
-            put_tile(tx, ty, &spu.ctile, TAG_SURFACE_CLEAR, 1);
+            put_tile(tx, ty, &spu.ztile[0], TAG_TILE_COLOR, 1);
       }
    }
 
diff --git a/src/gallium/drivers/cell/spu/spu_tile.h 
b/src/gallium/drivers/cell/spu/spu_tile.h
index 7bfb52b..e2733eb 100644
--- a/src/gallium/drivers/cell/spu/spu_tile.h
+++ b/src/gallium/drivers/cell/spu/spu_tile.h
@@ -37,7 +37,7 @@
 
 
 extern void
-get_tile(uint tx, uint ty, tile_t *tile, int tag, int zBuf);
+get_tile_f(uint tx, uint ty, tile_t *tile, int tag, int zBuf);
 
 extern void
 put_tile(uint tx, uint ty, const tile_t *tile, int tag, int zBuf);
diff --git a/src/gallium/drivers/cell/spu/spu_tri.c 
b/src/gallium/drivers/cell/spu/spu_tri.c
index c494706..22dfadd 100644
--- a/src/gallium/drivers/cell/spu/spu_tri.c
+++ b/src/gallium/drivers/cell/spu/spu_tri.c
@@ -295,7 +295,7 @@ emit_quad( int x, int y, mask_t mask)
           * very different.)  So choose the correct function depending
           * on the calculated facing.
           */
-         spu.fragment_ops[setup.facing](ix, iy, &spu.ctile, &spu.ztile,
+         spu.fragment_ops[setup.facing](ix, iy, &spu.ctile[spu.buffer], 
&spu.ztile[spu.buffer],
                           fragZ,
                           outputs[0*4+0],
                           outputs[0*4+1],
@@ -354,39 +354,8 @@ flush_spans(void)
       return;
    }
 
-   /* OK, we're very likely to need the tile data now.
-    * clear or finish waiting if needed.
-    */
-   if (spu.cur_ctile_status == TILE_STATUS_GETTING) {
-      /* wait for mfc_get() to complete */
-      //printf("SPU: %u: waiting for ctile\n", spu.init.id);
-      wait_on_mask(1 << TAG_READ_TILE_COLOR);
-      spu.cur_ctile_status = TILE_STATUS_CLEAN;
-   }
-   else if (spu.cur_ctile_status == TILE_STATUS_CLEAR) {
-      //printf("SPU %u: clearing C tile %u, %u\n", spu.init.id, setup.tx, 
setup.ty);
-      clear_c_tile(&spu.ctile);
-      spu.cur_ctile_status = TILE_STATUS_DIRTY;
-   }
-   ASSERT(spu.cur_ctile_status != TILE_STATUS_DEFINED);
-
-   if (spu.read_depth_stencil) {
-      if (spu.cur_ztile_status == TILE_STATUS_GETTING) {
-         /* wait for mfc_get() to complete */
-         //printf("SPU: %u: waiting for ztile\n", spu.init.id);
-         wait_on_mask(1 << TAG_READ_TILE_Z);
-         spu.cur_ztile_status = TILE_STATUS_CLEAN;
-      }
-      else if (spu.cur_ztile_status == TILE_STATUS_CLEAR) {
-         //printf("SPU %u: clearing Z tile %u, %u\n", spu.init.id, setup.tx, 
setup.ty);
-         clear_z_tile(&spu.ztile);
-         spu.cur_ztile_status = TILE_STATUS_DIRTY;
-      }
-      ASSERT(spu.cur_ztile_status != TILE_STATUS_DEFINED);
-   }
-
    /* XXX this loop could be moved into the above switch cases... */
-   
+
    /* Setup for mask calculation */
    const vec_int4 quad_LlRr = setup.span.quad;
    const vec_int4 quad_RrLl = spu_rlqwbyte(quad_LlRr, 8);
------------------------------------------------------------------------------
Register Now for Creativity and Technology (CaT), June 3rd, NYC. CaT
is a gathering of tech-side developers & brand creativity professionals. Meet
the minds behind Google Creative Lab, Visual Complexity, Processing, & 
iPhoneDevCamp asthey present alongside digital heavyweights like Barbarian
Group, R/GA, & Big Spaceship. http://www.creativitycat.com 
_______________________________________________
Mesa3d-dev mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/mesa3d-dev

Reply via email to