Detail texture geometry fixes, and offscreen loading tweak

Changes in support of Simon Wilson's work on improving the
Books carousel appearance. Provide an option to center the
detail texture with respect to the card, as opposed to the
default left-alignment. Draw the detail texture based on the
bounding box of the card, not just on the left coordinate.
Add an option to specify how many offscreen cards are
maintained in memory (as opposed to just one on each side).

Change-Id: I696f7315423440a9cecb503f07be96ffee96424f
diff --git a/carousel/java/com/android/ex/carousel/CarouselController.java b/carousel/java/com/android/ex/carousel/CarouselController.java
index 545b25c..ca0c5f8 100644
--- a/carousel/java/com/android/ex/carousel/CarouselController.java
+++ b/carousel/java/com/android/ex/carousel/CarouselController.java
@@ -39,6 +39,7 @@
     private final int DEFAULT_SLOT_COUNT = 10;
     private final float DEFAULT_RADIUS = 20.0f;
     private final int DEFAULT_VISIBLE_DETAIL_COUNT = 3;
+    private final int DEFAULT_PREFETCH_CARD_COUNT = 2;
     private final float DEFAULT_SWAY_SENSITIVITY = 0.0f;
     private final float DEFAULT_FRICTION_COEFFICIENT = 10.0f;
     private final float DEFAULT_DRAG_FACTOR = 0.25f;
@@ -60,7 +61,9 @@
     private int mCardCount = 0;
     private int mVisibleSlots = 0;
     private int mVisibleDetails = DEFAULT_VISIBLE_DETAIL_COUNT;
+    private int mPrefetchCardCount = DEFAULT_PREFETCH_CARD_COUNT;
     private boolean mDrawDetailBelowCard = false;
+    private boolean mDetailTexturesCentered = false;
     private boolean mDrawRuler = true;
     private float mStartAngle;
     private float mRadius = DEFAULT_RADIUS;
@@ -94,6 +97,7 @@
         createCards(mCardCount);
         setVisibleSlots(mVisibleSlots);
         setVisibleDetails(mVisibleDetails);
+        setPrefetchCardCount(mPrefetchCardCount);
         setDrawDetailBelowCard(mDrawDetailBelowCard);
         setDrawRuler(mDrawRuler);
         setCallback(mCarouselCallback);
@@ -182,6 +186,22 @@
     }
 
     /**
+     * Set the number of cards to pre-load that are outside of the visible region, as determined by
+     * setVisibleSlots(). This number gets added to the number of visible slots and used to
+     * determine when resources for cards should be loaded. This number should be small (n <= 4)
+     * for systems with limited texture memory or views that show more than half dozen cards in the
+     * view.
+     *
+     * @param n the number of cards; should be even, so the count is the same on each side
+     */
+    public void setPrefetchCardCount(int n) {
+        mPrefetchCardCount = n;
+        if (mRenderScript != null) {
+            mRenderScript.setPrefetchCardCount(n);
+        }
+    }
+
+    /**
      * Set whether to draw the detail texture above or below the card.
      *
      * @param below False for above, true for below.
@@ -194,6 +214,19 @@
     }
 
     /**
+     * Set whether to align the detail texture center with the card center.
+     * If not, left edges will be aligned instead.
+     *
+     * @param centered True for center-aligned, false for left-aligned.
+     */
+    public void setDetailTexturesCentered(boolean centered) {
+        mDetailTexturesCentered = centered;
+        if (mRenderScript != null) {
+            mRenderScript.setDetailTexturesCentered(centered);
+        }
+    }
+
+    /**
      * Set whether to draw a ruler from the card to the detail texture
      *
      * @param drawRuler True to draw a ruler, false to draw nothing where the ruler would go.
@@ -315,7 +348,7 @@
 
     /**
      * Can be used to optionally set the background to a bitmap. When set to something other than
-     * null, this overrides {@link CarouselView#setBackgroundColor(Float4)}.
+     * null, this overrides {@link CarouselController#setBackgroundColor(Float4)}.
      *
      * @param bitmap
      */
diff --git a/carousel/java/com/android/ex/carousel/CarouselRS.java b/carousel/java/com/android/ex/carousel/CarouselRS.java
index cbb6714..134057c 100644
--- a/carousel/java/com/android/ex/carousel/CarouselRS.java
+++ b/carousel/java/com/android/ex/carousel/CarouselRS.java
@@ -368,10 +368,19 @@
         mScript.set_visibleDetailCount(count);
     }
 
+    public void setPrefetchCardCount(int count) {
+        mScript.set_prefetchCardCount(count);
+    }
+
+
     public void setDrawDetailBelowCard(boolean below) {
         mScript.set_drawDetailBelowCard(below);
     }
 
+    public void setDetailTexturesCentered(boolean centered) {
+        mScript.set_detailTexturesCentered(centered);
+    }
+
     public void setDrawRuler(boolean drawRuler) {
         mScript.set_drawRuler(drawRuler);
     }
diff --git a/carousel/java/com/android/ex/carousel/CarouselView.java b/carousel/java/com/android/ex/carousel/CarouselView.java
index 6073dcb..7eaecec 100644
--- a/carousel/java/com/android/ex/carousel/CarouselView.java
+++ b/carousel/java/com/android/ex/carousel/CarouselView.java
@@ -47,6 +47,7 @@
     private final int DEFAULT_SLOT_COUNT = 10;
     private final float DEFAULT_RADIUS = 20.0f;
     private final int DEFAULT_VISIBLE_DETAIL_COUNT = 3;
+    private final int DEFAULT_PREFETCH_CARD_COUNT = 2;
     private final float DEFAULT_SWAY_SENSITIVITY = 0.0f;
     private final float DEFAULT_FRICTION_COEFFICIENT = 10.0f;
     private final float DEFAULT_DRAG_FACTOR = 0.25f;
@@ -69,7 +70,9 @@
     private int mCardCount = 0;
     private int mVisibleSlots = 0;
     private int mVisibleDetails = DEFAULT_VISIBLE_DETAIL_COUNT;
+    private int mPrefetchCardCount = DEFAULT_PREFETCH_CARD_COUNT;
     private boolean mDrawDetailBelowCard = false;
+    private boolean mDetailTexturesCentered = false;
     private boolean mDrawRuler = true;
     private float mStartAngle;
     private float mRadius = DEFAULT_RADIUS;
@@ -143,6 +146,7 @@
         createCards(mCardCount);
         setVisibleSlots(mVisibleSlots);
         setVisibleDetails(mVisibleDetails);
+        setPrefetchCardCount(mPrefetchCardCount);
         setDrawDetailBelowCard(mDrawDetailBelowCard);
         setDrawRuler(mDrawRuler);
         setCallback(mCarouselCallback);
@@ -243,6 +247,22 @@
     }
 
     /**
+     * Set the number of cards to pre-load that are outside of the visible region, as determined by
+     * setVisibleSlots(). This number gets added to the number of visible slots and used to
+     * determine when resources for cards should be loaded. This number should be small (n <= 4)
+     * for systems with limited texture memory or views that show more than half dozen cards in the
+     * view.
+     *
+     * @param n the number of cards; should be even, so the count is the same on each side
+     */
+    public void setPrefetchCardCount(int n) {
+        mPrefetchCardCount = n;
+        if (mRenderScript != null) {
+            mRenderScript.setPrefetchCardCount(n);
+        }
+    }
+
+    /**
      * Set whether to draw the detail texture above or below the card.
      *
      * @param below False for above, true for below.
@@ -255,6 +275,19 @@
     }
 
     /**
+     * Set whether to align the detail texture center with the card center.
+     * If not, left edges will be aligned instead.
+     *
+     * @param centered True for center-aligned, false for left-aligned.
+     */
+    public void setDetailTexturesCentered(boolean centered) {
+        mDetailTexturesCentered = centered;
+        if (mRenderScript != null) {
+            mRenderScript.setDetailTexturesCentered(centered);
+        }
+    }
+
+    /**
      * Set whether to draw a ruler from the card to the detail texture
      *
      * @param drawRuler True to draw a ruler, false to draw nothing where the ruler would go.
diff --git a/carousel/java/com/android/ex/carousel/MVCCarouselView.java b/carousel/java/com/android/ex/carousel/MVCCarouselView.java
index ba67f97..2f022c1 100644
--- a/carousel/java/com/android/ex/carousel/MVCCarouselView.java
+++ b/carousel/java/com/android/ex/carousel/MVCCarouselView.java
@@ -176,6 +176,19 @@
     }
 
     /**
+     * Set the number of cards to pre-load that are outside of the visible region, as determined by
+     * setVisibleSlots(). This number gets added to the number of visible slots and used to
+     * determine when resources for cards should be loaded. This number should be small (n <= 4)
+     * for systems with limited texture memory or views that show more than half dozen cards in the
+     * view.
+     *
+     * @param n the number of cards; should be even, so the count is the same on each side
+     */
+    public void setPrefetchCardCount(int n) {
+        mController.setPrefetchCardCount(n);
+    }
+
+    /**
      * Set the number of detail textures that can be visible at one time.
      *
      * @param n the number of slots
@@ -194,6 +207,16 @@
     }
 
     /**
+     * Set whether to align the detail texture center with the card center.
+     * If not, left edges will be aligned instead.
+     *
+     * @param centered True for center-aligned, false for left-aligned.
+     */
+    public void setDetailTexturesCentered(boolean centered) {
+        mController.setDetailTexturesCentered(centered);
+    }
+
+    /**
      * Set whether to draw a ruler from the card to the detail texture
      *
      * @param drawRuler True to draw a ruler, false to draw nothing where the ruler would go.
diff --git a/carousel/java/com/android/ex/carousel/carousel.rs b/carousel/java/com/android/ex/carousel/carousel.rs
index 551795b..612ba92 100644
--- a/carousel/java/com/android/ex/carousel/carousel.rs
+++ b/carousel/java/com/android/ex/carousel/carousel.rs
@@ -99,7 +99,10 @@
 int cardCount; // number of cards in stack
 int visibleSlotCount; // number of visible slots (for culling)
 int visibleDetailCount; // number of visible detail textures to show
+int prefetchCardCount; // how many cards to keep in memory
 bool drawDetailBelowCard; // whether detail goes above (false) or below (true) the card
+// TODO(jshuma): Replace detailTexturesCentered with a detailTextureAlignment mode enum
+bool detailTexturesCentered; // line up detail center and card center (instead of left edges)
 bool drawRuler; // whether to draw a ruler from the card to the detail texture
 float radius; // carousel radius. Cards will be centered on a circle with this radius
 float cardRotation; // rotation of card in XY plane relative to Z=1
@@ -536,6 +539,28 @@
     return stillAnimating;
 }
 
+/**
+ * Convert projection from normalized coordinates to pixel coordinates.
+ *
+ * @return True on success, false on failure.
+ */
+static bool convertNormalizedToPixelCoordinates(float4 *screenCoord, float width, float height) {
+    // This is probably cheaper than pre-multiplying with another matrix.
+    if (screenCoord->w == 0.0f) {
+        rsDebug("Bad transform while converting from normalized to pixel coordinates: ",
+            screenCoord);
+        return false;
+    }
+    *screenCoord *= 1.0f / screenCoord->w;
+    screenCoord->x += 1.0f;
+    screenCoord->y += 1.0f;
+    screenCoord->z += 1.0f;
+    screenCoord->x = round(screenCoord->x * 0.5f * width);
+    screenCoord->y = round(screenCoord->y * 0.5f * height);
+    screenCoord->z = - 0.5f * screenCoord->z;
+    return true;
+}
+
 /*
  * Draws a screen-aligned card with the exact dimensions from the detail texture.
  * This is used to display information about the object being displayed above the geomertry.
@@ -572,19 +597,42 @@
             if (cards[i].detailTextureState == STATE_LOADED && cards[i].detailTexture.p != 0) {
                 const float lineWidth = rsAllocationGetDimX(detailLineTexture);
 
-                // Compute position in screen space of upper left corner of card
+                // Compute position in screen space of top corner or bottom corner of card
                 rsMatrixLoad(&model, &modelviewMatrix);
                 getMatrixForCard(&model, i, false);
                 rs_matrix4x4 matrix;
                 rsMatrixLoadMultiply(&matrix, &projectionMatrix, &model);
 
-                float4 screenCoord = rsMatrixMultiply(&matrix,
-                    cardVertices[drawDetailBelowCard ? 0 : 3]);
-                if (screenCoord.w == 0.0f) {
+                int indexLeft, indexRight;
+                float4 screenCoord;
+                if (drawDetailBelowCard) {
+                    indexLeft = 0;
+                    indexRight = 1;
+                } else {
+                    indexLeft = 3;
+                    indexRight = 2;
+                }
+                float4 screenCoordLeft = rsMatrixMultiply(&matrix, cardVertices[indexLeft]);
+                float4 screenCoordRight = rsMatrixMultiply(&matrix, cardVertices[indexRight]);
+                if (screenCoordLeft.w == 0.0f || screenCoordRight.w == 0.0f) {
                     // this shouldn't happen
                     rsDebug("Bad transform: ", screenCoord);
                     continue;
                 }
+                (void) convertNormalizedToPixelCoordinates(&screenCoordLeft, width, height);
+                (void) convertNormalizedToPixelCoordinates(&screenCoordRight, width, height);
+                if (debugDetails) {
+                    RS_DEBUG(screenCoordLeft);
+                    RS_DEBUG(screenCoordRight);
+                }
+                screenCoord = screenCoordLeft;
+                if (drawDetailBelowCard) {
+                    screenCoord.y = min(screenCoordLeft.y, screenCoordRight.y);
+                }
+                if (detailTexturesCentered) {
+                    screenCoord.x += (screenCoordRight.x - screenCoordLeft.x) / 2. -
+                        rsAllocationGetDimX(cards[i].detailTexture) / 2.;
+                }
 
                 // Compute alpha for gradually fading in details. Applied to both line and
                 // detail texture. TODO: use a separate background texture for line.
@@ -612,19 +660,6 @@
                 shaderConstants->fadeAmount = blendedAlpha;
                 rsAllocationMarkDirty(rsGetAllocation(shaderConstants));
 
-                // Convert projection from normalized coordinates to pixel coordinates.
-                // This is probably cheaper than pre-multiplying the above with another matrix.
-                screenCoord *= 1.0f / screenCoord.w;
-                screenCoord.x += 1.0f;
-                screenCoord.y += 1.0f;
-                screenCoord.z += 1.0f;
-                screenCoord.x = round(screenCoord.x * 0.5f * width);
-                screenCoord.y = round(screenCoord.y * 0.5f * height);
-                screenCoord.z = - 0.5f * screenCoord.z;
-                if (debugDetails) {
-                    RS_DEBUG(screenCoord);
-                }
-
                 // Draw line from upper left card corner to the top of the screen
                 if (drawRuler) {
                     const float halfWidth = lineWidth * 0.5f;
@@ -1050,12 +1085,16 @@
 // Otherwise, it should cull based on bounds of geometry.
 static int cullCards()
 {
-    const float thetaFirst = slotPosition(-1); // -1 keeps the card in front around a bit longer
+    // TODO(jshuma): Instead of fully fetching prefetchCardCount cards, make a distinction between
+    // STATE_LOADED and a new STATE_PRELOADING, which will keep the textures loaded but will not
+    // attempt to actually draw them.
+    const int prefetchCardCountPerSide = prefetchCardCount / 2;
+    const float thetaFirst = slotPosition(-prefetchCardCountPerSide);
     const float thetaSelected = slotPosition(0);
     const float thetaHalfAngle = (thetaSelected - thetaFirst) * 0.5f;
     const float thetaSelectedLow = thetaSelected - thetaHalfAngle;
     const float thetaSelectedHigh = thetaSelected + thetaHalfAngle;
-    const float thetaLast = slotPosition(visibleSlotCount);
+    const float thetaLast = slotPosition(visibleSlotCount - 1 + prefetchCardCountPerSide);
 
     int count = 0;
     int firstVisible = -1;