Home | History | Annotate | Download | only in server
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageInfo;
     21 import android.content.pm.PackageManager;
     22 import android.content.res.Resources;
     23 import android.graphics.Atlas;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.PixelFormat;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.PorterDuffXfermode;
     30 import android.graphics.drawable.Drawable;
     31 import android.os.Environment;
     32 import android.os.RemoteException;
     33 import android.os.SystemProperties;
     34 import android.util.Log;
     35 import android.util.LongSparseArray;
     36 import android.view.GraphicBuffer;
     37 import android.view.IAssetAtlas;
     38 
     39 import java.io.BufferedReader;
     40 import java.io.BufferedWriter;
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.io.InputStreamReader;
     47 import java.io.OutputStreamWriter;
     48 import java.util.ArrayList;
     49 import java.util.Collection;
     50 import java.util.Collections;
     51 import java.util.Comparator;
     52 import java.util.HashSet;
     53 import java.util.List;
     54 import java.util.concurrent.CountDownLatch;
     55 import java.util.concurrent.TimeUnit;
     56 import java.util.concurrent.atomic.AtomicBoolean;
     57 
     58 /**
     59  * This service is responsible for packing preloaded bitmaps into a single
     60  * atlas texture. The resulting texture can be shared across processes to
     61  * reduce overall memory usage.
     62  *
     63  * @hide
     64  */
     65 public class AssetAtlasService extends IAssetAtlas.Stub {
     66     /**
     67      * Name of the <code>AssetAtlasService</code>.
     68      */
     69     public static final String ASSET_ATLAS_SERVICE = "assetatlas";
     70 
     71     private static final String LOG_TAG = "AssetAtlas";
     72 
     73     // Turns debug logs on/off. Debug logs are kept to a minimum and should
     74     // remain on to diagnose issues
     75     private static final boolean DEBUG_ATLAS = true;
     76 
     77     // When set to true the content of the atlas will be saved to disk
     78     // in /data/system/atlas.png. The shared GraphicBuffer may be empty
     79     private static final boolean DEBUG_ATLAS_TEXTURE = false;
     80 
     81     // Minimum size in pixels to consider for the resulting texture
     82     private static final int MIN_SIZE = 512;
     83     // Maximum size in pixels to consider for the resulting texture
     84     private static final int MAX_SIZE = 2048;
     85     // Increment in number of pixels between size variants when looking
     86     // for the best texture dimensions
     87     private static final int STEP = 64;
     88 
     89     // This percentage of the total number of pixels represents the minimum
     90     // number of pixels we want to be able to pack in the atlas
     91     private static final float PACKING_THRESHOLD = 0.8f;
     92 
     93     // Defines the number of int fields used to represent a single entry
     94     // in the atlas map. This number defines the size of the array returned
     95     // by the getMap(). See the mAtlasMap field for more information
     96     private static final int ATLAS_MAP_ENTRY_FIELD_COUNT = 3;
     97 
     98     // Specifies how our GraphicBuffer will be used. To get proper swizzling
     99     // the buffer will be written to using OpenGL (from JNI) so we can leave
    100     // the software flag set to "never"
    101     private static final int GRAPHIC_BUFFER_USAGE = GraphicBuffer.USAGE_SW_READ_NEVER |
    102             GraphicBuffer.USAGE_SW_WRITE_NEVER | GraphicBuffer.USAGE_HW_TEXTURE;
    103 
    104     // This boolean is set to true if an atlas was successfully
    105     // computed and rendered
    106     private final AtomicBoolean mAtlasReady = new AtomicBoolean(false);
    107 
    108     private final Context mContext;
    109 
    110     // Version name of the current build, used to identify changes to assets list
    111     private final String mVersionName;
    112 
    113     // Holds the atlas' data. This buffer can be mapped to
    114     // OpenGL using an EGLImage
    115     private GraphicBuffer mBuffer;
    116 
    117     // Describes how bitmaps are placed in the atlas. Each bitmap is
    118     // represented by several entries in the array:
    119     // long0: SkBitmap*, the native bitmap object
    120     // long1: x position
    121     // long2: y position
    122     private long[] mAtlasMap;
    123 
    124     /**
    125      * Creates a new service. Upon creating, the service will gather the list of
    126      * assets to consider for packing into the atlas and spawn a new thread to
    127      * start the packing work.
    128      *
    129      * @param context The context giving access to preloaded resources
    130      */
    131     public AssetAtlasService(Context context) {
    132         mContext = context;
    133         mVersionName = queryVersionName(context);
    134 
    135         Collection<Bitmap> bitmaps = new HashSet<Bitmap>(300);
    136         int totalPixelCount = 0;
    137 
    138         // We only care about drawables that hold bitmaps
    139         final Resources resources = context.getResources();
    140         final LongSparseArray<Drawable.ConstantState> drawables = resources.getPreloadedDrawables();
    141 
    142         final int count = drawables.size();
    143         for (int i = 0; i < count; i++) {
    144             try {
    145                 totalPixelCount += drawables.valueAt(i).addAtlasableBitmaps(bitmaps);
    146             } catch (Throwable t) {
    147                 Log.e("AssetAtlas", "Failed to fetch preloaded drawable state", t);
    148                 throw t;
    149             }
    150         }
    151 
    152         ArrayList<Bitmap> sortedBitmaps = new ArrayList<Bitmap>(bitmaps);
    153         // Our algorithms perform better when the bitmaps are first sorted
    154         // The comparator will sort the bitmap by width first, then by height
    155         Collections.sort(sortedBitmaps, new Comparator<Bitmap>() {
    156             @Override
    157             public int compare(Bitmap b1, Bitmap b2) {
    158                 if (b1.getWidth() == b2.getWidth()) {
    159                     return b2.getHeight() - b1.getHeight();
    160                 }
    161                 return b2.getWidth() - b1.getWidth();
    162             }
    163         });
    164 
    165         // Kick off the packing work on a worker thread
    166         new Thread(new Renderer(sortedBitmaps, totalPixelCount)).start();
    167     }
    168 
    169     /**
    170      * Queries the version name stored in framework's AndroidManifest.
    171      * The version name can be used to identify possible changes to
    172      * framework resources.
    173      *
    174      * @see #getBuildIdentifier(String)
    175      */
    176     private static String queryVersionName(Context context) {
    177         try {
    178             String packageName = context.getPackageName();
    179             PackageInfo info = context.getPackageManager().getPackageInfo(packageName,
    180                     PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
    181             return info.versionName;
    182         } catch (PackageManager.NameNotFoundException e) {
    183             Log.w(LOG_TAG, "Could not get package info", e);
    184         }
    185         return null;
    186     }
    187 
    188     /**
    189      * Callback invoked by the server thread to indicate we can now run
    190      * 3rd party code.
    191      */
    192     public void systemRunning() {
    193     }
    194 
    195     /**
    196      * The renderer does all the work:
    197      */
    198     private class Renderer implements Runnable {
    199         private final ArrayList<Bitmap> mBitmaps;
    200         private final int mPixelCount;
    201 
    202         Renderer(ArrayList<Bitmap> bitmaps, int pixelCount) {
    203             mBitmaps = bitmaps;
    204             mPixelCount = pixelCount;
    205         }
    206 
    207         /**
    208          * 1. On first boot or after every update, brute-force through all the
    209          *    possible atlas configurations and look for the best one (maximimize
    210          *    number of packed assets and minimize texture size)
    211          *    a. If a best configuration was computed, write it out to disk for
    212          *       future use
    213          * 2. Read best configuration from disk
    214          * 3. Compute the packing using the best configuration
    215          * 4. Allocate a GraphicBuffer
    216          * 5. Render assets in the buffer
    217          */
    218         @Override
    219         public void run() {
    220             Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName);
    221             if (DEBUG_ATLAS) Log.d(LOG_TAG, "Loaded configuration: " + config);
    222 
    223             if (config != null) {
    224                 mBuffer = GraphicBuffer.create(config.width, config.height,
    225                         PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE);
    226 
    227                 if (mBuffer != null) {
    228                     Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags);
    229                     if (renderAtlas(mBuffer, atlas, config.count)) {
    230                         mAtlasReady.set(true);
    231                     }
    232                 }
    233             }
    234         }
    235 
    236         /**
    237          * Renders a list of bitmaps into the atlas. The position of each bitmap
    238          * was decided by the packing algorithm and will be honored by this
    239          * method.
    240          *
    241          * @param buffer The buffer to render the atlas entries into
    242          * @param atlas The atlas to pack the bitmaps into
    243          * @param packCount The number of bitmaps that will be packed in the atlas
    244          *
    245          * @return true if the atlas was rendered, false otherwise
    246          */
    247         @SuppressWarnings("MismatchedReadAndWriteOfArray")
    248         private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) {
    249             // Use a Source blend mode to improve performance, the target bitmap
    250             // will be zero'd out so there's no need to waste time applying blending
    251             final Paint paint = new Paint();
    252             paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
    253 
    254             // We always render the atlas into a bitmap. This bitmap is then
    255             // uploaded into the GraphicBuffer using OpenGL to swizzle the content
    256             final Bitmap atlasBitmap = Bitmap.createBitmap(
    257                     buffer.getWidth(), buffer.getHeight(), Bitmap.Config.ARGB_8888);
    258             final Canvas canvas = new Canvas(atlasBitmap);
    259 
    260             final Atlas.Entry entry = new Atlas.Entry();
    261 
    262             mAtlasMap = new long[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT];
    263             long[] atlasMap = mAtlasMap;
    264             int mapIndex = 0;
    265 
    266             boolean result = false;
    267             final long startRender = System.nanoTime();
    268             final int count = mBitmaps.size();
    269 
    270             for (int i = 0; i < count; i++) {
    271                 final Bitmap bitmap = mBitmaps.get(i);
    272                 if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) {
    273                     // We have more bitmaps to pack than the current configuration
    274                     // says, we were most likely not able to detect a change in the
    275                     // list of preloaded drawables, abort and delete the configuration
    276                     if (mapIndex >= mAtlasMap.length) {
    277                         deleteDataFile();
    278                         break;
    279                     }
    280 
    281                     canvas.save();
    282                     canvas.translate(entry.x, entry.y);
    283                     canvas.drawBitmap(bitmap, 0.0f, 0.0f, null);
    284                     canvas.restore();
    285                     atlasMap[mapIndex++] = bitmap.refSkPixelRef();
    286                     atlasMap[mapIndex++] = entry.x;
    287                     atlasMap[mapIndex++] = entry.y;
    288                 }
    289             }
    290 
    291             final long endRender = System.nanoTime();
    292             releaseCanvas(canvas, atlasBitmap);
    293             result = nUploadAtlas(buffer, atlasBitmap);
    294             atlasBitmap.recycle();
    295             final long endUpload = System.nanoTime();
    296 
    297             if (DEBUG_ATLAS) {
    298                 float renderDuration = (endRender - startRender) / 1000.0f / 1000.0f;
    299                 float uploadDuration = (endUpload - endRender) / 1000.0f / 1000.0f;
    300                 Log.d(LOG_TAG, String.format("Rendered atlas in %.2fms (%.2f+%.2fms)",
    301                         renderDuration + uploadDuration, renderDuration, uploadDuration));
    302             }
    303 
    304             return result;
    305         }
    306 
    307         /**
    308          * Releases the canvas used to render into the buffer. Calling this method
    309          * will release any resource previously acquired. If {@link #DEBUG_ATLAS_TEXTURE}
    310          * is turend on, calling this method will write the content of the atlas
    311          * to disk in /data/system/atlas.png for debugging.
    312          */
    313         private void releaseCanvas(Canvas canvas, Bitmap atlasBitmap) {
    314             canvas.setBitmap(null);
    315             if (DEBUG_ATLAS_TEXTURE) {
    316 
    317                 File systemDirectory = new File(Environment.getDataDirectory(), "system");
    318                 File dataFile = new File(systemDirectory, "atlas.png");
    319 
    320                 try {
    321                     FileOutputStream out = new FileOutputStream(dataFile);
    322                     atlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
    323                     out.close();
    324                 } catch (FileNotFoundException e) {
    325                     // Ignore
    326                 } catch (IOException e) {
    327                     // Ignore
    328                 }
    329             }
    330         }
    331     }
    332 
    333     private static native boolean nUploadAtlas(GraphicBuffer buffer, Bitmap bitmap);
    334 
    335     @Override
    336     public boolean isCompatible(int ppid) {
    337         return ppid == android.os.Process.myPpid();
    338     }
    339 
    340     @Override
    341     public GraphicBuffer getBuffer() throws RemoteException {
    342         return mAtlasReady.get() ? mBuffer : null;
    343     }
    344 
    345     @Override
    346     public long[] getMap() throws RemoteException {
    347         return mAtlasReady.get() ? mAtlasMap : null;
    348     }
    349 
    350     /**
    351      * Finds the best atlas configuration to pack the list of supplied bitmaps.
    352      * This method takes advantage of multi-core systems by spawning a number
    353      * of threads equal to the number of available cores.
    354      */
    355     private static Configuration computeBestConfiguration(
    356             ArrayList<Bitmap> bitmaps, int pixelCount) {
    357         if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration...");
    358 
    359         long begin = System.nanoTime();
    360         List<WorkerResult> results = Collections.synchronizedList(new ArrayList<WorkerResult>());
    361 
    362         // Don't bother with an extra thread if there's only one processor
    363         int cpuCount = Runtime.getRuntime().availableProcessors();
    364         if (cpuCount == 1) {
    365             new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run();
    366         } else {
    367             int start = MIN_SIZE + (cpuCount - 1) * STEP;
    368             int end = MAX_SIZE;
    369             int step = STEP * cpuCount;
    370 
    371             final CountDownLatch signal = new CountDownLatch(cpuCount);
    372 
    373             for (int i = 0; i < cpuCount; i++, start -= STEP, end -= STEP) {
    374                 ComputeWorker worker = new ComputeWorker(start, end, step,
    375                         bitmaps, pixelCount, results, signal);
    376                 new Thread(worker, "Atlas Worker #" + (i + 1)).start();
    377             }
    378 
    379             boolean isAllWorkerFinished;
    380             try {
    381                 isAllWorkerFinished = signal.await(10, TimeUnit.SECONDS);
    382             } catch (InterruptedException e) {
    383                 Log.w(LOG_TAG, "Could not complete configuration computation");
    384                 return null;
    385             }
    386 
    387             if (!isAllWorkerFinished) {
    388                 // We have to abort here, otherwise the async updates on "results" would crash the
    389                 // sort later.
    390                 Log.w(LOG_TAG, "Could not complete configuration computation before timeout.");
    391                 return null;
    392             }
    393         }
    394 
    395         // Maximize the number of packed bitmaps, minimize the texture size
    396         Collections.sort(results, new Comparator<WorkerResult>() {
    397             @Override
    398             public int compare(WorkerResult r1, WorkerResult r2) {
    399                 int delta = r2.count - r1.count;
    400                 if (delta != 0) return delta;
    401                 return r1.width * r1.height - r2.width * r2.height;
    402             }
    403         });
    404 
    405         if (DEBUG_ATLAS) {
    406             float delay = (System.nanoTime() - begin) / 1000.0f / 1000.0f / 1000.0f;
    407             Log.d(LOG_TAG, String.format("Found best atlas configuration (out of %d) in %.2fs",
    408                     results.size(), delay));
    409         }
    410 
    411         WorkerResult result = results.get(0);
    412         return new Configuration(result.type, result.width, result.height, result.count);
    413     }
    414 
    415     /**
    416      * Returns the path to the file containing the best computed
    417      * atlas configuration.
    418      */
    419     private static File getDataFile() {
    420         File systemDirectory = new File(Environment.getDataDirectory(), "system");
    421         return new File(systemDirectory, "framework_atlas.config");
    422     }
    423 
    424     private static void deleteDataFile() {
    425         Log.w(LOG_TAG, "Current configuration inconsistent with assets list");
    426         if (!getDataFile().delete()) {
    427             Log.w(LOG_TAG, "Could not delete the current configuration");
    428         }
    429     }
    430 
    431     private File getFrameworkResourcesFile() {
    432         return new File(mContext.getApplicationInfo().sourceDir);
    433     }
    434 
    435     /**
    436      * Returns the best known atlas configuration. This method will either
    437      * read the configuration from disk or start a brute-force search
    438      * and save the result out to disk.
    439      */
    440     private Configuration chooseConfiguration(ArrayList<Bitmap> bitmaps, int pixelCount,
    441             String versionName) {
    442         Configuration config = null;
    443 
    444         final File dataFile = getDataFile();
    445         if (dataFile.exists()) {
    446             config = readConfiguration(dataFile, versionName);
    447         }
    448 
    449         if (config == null) {
    450             config = computeBestConfiguration(bitmaps, pixelCount);
    451             if (config != null) writeConfiguration(config, dataFile, versionName);
    452         }
    453 
    454         return config;
    455     }
    456 
    457     /**
    458      * Writes the specified atlas configuration to the specified file.
    459      */
    460     private void writeConfiguration(Configuration config, File file, String versionName) {
    461         BufferedWriter writer = null;
    462         try {
    463             writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
    464             writer.write(getBuildIdentifier(versionName));
    465             writer.newLine();
    466             writer.write(config.type.toString());
    467             writer.newLine();
    468             writer.write(String.valueOf(config.width));
    469             writer.newLine();
    470             writer.write(String.valueOf(config.height));
    471             writer.newLine();
    472             writer.write(String.valueOf(config.count));
    473             writer.newLine();
    474             writer.write(String.valueOf(config.flags));
    475             writer.newLine();
    476         } catch (FileNotFoundException e) {
    477             Log.w(LOG_TAG, "Could not write " + file, e);
    478         } catch (IOException e) {
    479             Log.w(LOG_TAG, "Could not write " + file, e);
    480         } finally {
    481             if (writer != null) {
    482                 try {
    483                     writer.close();
    484                 } catch (IOException e) {
    485                     // Ignore
    486                 }
    487             }
    488         }
    489     }
    490 
    491     /**
    492      * Reads an atlas configuration from the specified file. This method
    493      * returns null if an error occurs or if the configuration is invalid.
    494      */
    495     private Configuration readConfiguration(File file, String versionName) {
    496         BufferedReader reader = null;
    497         Configuration config = null;
    498         try {
    499             reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
    500 
    501             if (checkBuildIdentifier(reader, versionName)) {
    502                 Atlas.Type type = Atlas.Type.valueOf(reader.readLine());
    503                 int width = readInt(reader, MIN_SIZE, MAX_SIZE);
    504                 int height = readInt(reader, MIN_SIZE, MAX_SIZE);
    505                 int count = readInt(reader, 0, Integer.MAX_VALUE);
    506                 int flags = readInt(reader, Integer.MIN_VALUE, Integer.MAX_VALUE);
    507 
    508                 config = new Configuration(type, width, height, count, flags);
    509             }
    510         } catch (IllegalArgumentException e) {
    511             Log.w(LOG_TAG, "Invalid parameter value in " + file, e);
    512         } catch (FileNotFoundException e) {
    513             Log.w(LOG_TAG, "Could not read " + file, e);
    514         } catch (IOException e) {
    515             Log.w(LOG_TAG, "Could not read " + file, e);
    516         } finally {
    517             if (reader != null) {
    518                 try {
    519                     reader.close();
    520                 } catch (IOException e) {
    521                     // Ignore
    522                 }
    523             }
    524         }
    525         return config;
    526     }
    527 
    528     private static int readInt(BufferedReader reader, int min, int max) throws IOException {
    529         return Math.max(min, Math.min(max, Integer.parseInt(reader.readLine())));
    530     }
    531 
    532     /**
    533      * Compares the next line in the specified buffered reader to the current
    534      * build identifier. Returns whether the two values are equal.
    535      *
    536      * @see #getBuildIdentifier(String)
    537      */
    538     private boolean checkBuildIdentifier(BufferedReader reader, String versionName)
    539             throws IOException {
    540         String deviceBuildId = getBuildIdentifier(versionName);
    541         String buildId = reader.readLine();
    542         return deviceBuildId.equals(buildId);
    543     }
    544 
    545     /**
    546      * Returns an identifier for the current build that can be used to detect
    547      * likely changes to framework resources. The build identifier is made of
    548      * several distinct values:
    549      *
    550      * build fingerprint/framework version name/file size of framework resources apk
    551      *
    552      * Only the build fingerprint should be necessary on user builds but
    553      * the other values are useful to detect changes on eng builds during
    554      * development.
    555      *
    556      * This identifier does not attempt to be exact: a new identifier does not
    557      * necessarily mean the preloaded drawables have changed. It is important
    558      * however that whenever the list of preloaded drawables changes, this
    559      * identifier changes as well.
    560      *
    561      * @see #checkBuildIdentifier(java.io.BufferedReader, String)
    562      */
    563     private String getBuildIdentifier(String versionName) {
    564         return SystemProperties.get("ro.build.fingerprint", "") + '/' + versionName + '/' +
    565                 String.valueOf(getFrameworkResourcesFile().length());
    566     }
    567 
    568     /**
    569      * Atlas configuration. Specifies the algorithm, dimensions and flags to use.
    570      */
    571     private static class Configuration {
    572         final Atlas.Type type;
    573         final int width;
    574         final int height;
    575         final int count;
    576         final int flags;
    577 
    578         Configuration(Atlas.Type type, int width, int height, int count) {
    579             this(type, width, height, count, Atlas.FLAG_DEFAULTS);
    580         }
    581 
    582         Configuration(Atlas.Type type, int width, int height, int count, int flags) {
    583             this.type = type;
    584             this.width = width;
    585             this.height = height;
    586             this.count = count;
    587             this.flags = flags;
    588         }
    589 
    590         @Override
    591         public String toString() {
    592             return type.toString() + " (" + width + "x" + height + ") flags=0x" +
    593                     Integer.toHexString(flags) + " count=" + count;
    594         }
    595     }
    596 
    597     /**
    598      * Used during the brute-force search to gather information about each
    599      * variant of the packing algorithm.
    600      */
    601     private static class WorkerResult {
    602         Atlas.Type type;
    603         int width;
    604         int height;
    605         int count;
    606 
    607         WorkerResult(Atlas.Type type, int width, int height, int count) {
    608             this.type = type;
    609             this.width = width;
    610             this.height = height;
    611             this.count = count;
    612         }
    613 
    614         @Override
    615         public String toString() {
    616             return String.format("%s %dx%d", type.toString(), width, height);
    617         }
    618     }
    619 
    620     /**
    621      * A compute worker will try a finite number of variations of the packing
    622      * algorithms and save the results in a supplied list.
    623      */
    624     private static class ComputeWorker implements Runnable {
    625         private final int mStart;
    626         private final int mEnd;
    627         private final int mStep;
    628         private final List<Bitmap> mBitmaps;
    629         private final List<WorkerResult> mResults;
    630         private final CountDownLatch mSignal;
    631         private final int mThreshold;
    632 
    633         /**
    634          * Creates a new compute worker to brute-force through a range of
    635          * packing algorithms variants.
    636          *
    637          * @param start The minimum texture width to try
    638          * @param end The maximum texture width to try
    639          * @param step The number of pixels to increment the texture width by at each step
    640          * @param bitmaps The list of bitmaps to pack in the atlas
    641          * @param pixelCount The total number of pixels occupied by the list of bitmaps
    642          * @param results The list of results in which to save the brute-force search results
    643          * @param signal Latch to decrement when this worker is done, may be null
    644          */
    645         ComputeWorker(int start, int end, int step, List<Bitmap> bitmaps, int pixelCount,
    646                 List<WorkerResult> results, CountDownLatch signal) {
    647             mStart = start;
    648             mEnd = end;
    649             mStep = step;
    650             mBitmaps = bitmaps;
    651             mResults = results;
    652             mSignal = signal;
    653 
    654             // Minimum number of pixels we want to be able to pack
    655             int threshold = (int) (pixelCount * PACKING_THRESHOLD);
    656             // Make sure we can find at least one configuration
    657             while (threshold > MAX_SIZE * MAX_SIZE) {
    658                 threshold >>= 1;
    659             }
    660             mThreshold = threshold;
    661         }
    662 
    663         @Override
    664         public void run() {
    665             if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName());
    666 
    667             Atlas.Entry entry = new Atlas.Entry();
    668 
    669             for (int width = mEnd; width > mStart; width -= mStep) {
    670                 for (int height = MAX_SIZE; height > MIN_SIZE; height -= STEP) {
    671                     // If the atlas is not big enough, skip it
    672                     if (width * height <= mThreshold) continue;
    673 
    674                     boolean packSuccess = false;
    675 
    676                     for (Atlas.Type type : Atlas.Type.values()) {
    677                         final int count = packBitmaps(type, width, height, entry);
    678                         if (count > 0) {
    679                             mResults.add(new WorkerResult(type, width, height, count));
    680                             if (count == mBitmaps.size()) {
    681                                 // If we were able to pack everything let's stop here
    682                                 // Changing the type further won't make things better
    683                                 packSuccess = true;
    684                                 break;
    685                             }
    686                         }
    687                     }
    688 
    689                     // If we were not able to pack everything let's stop here
    690                     // Decreasing the height further won't make things better
    691                     if (!packSuccess) {
    692                         break;
    693                     }
    694                 }
    695             }
    696 
    697             if (mSignal != null) {
    698                 mSignal.countDown();
    699             }
    700         }
    701 
    702         private int packBitmaps(Atlas.Type type, int width, int height, Atlas.Entry entry) {
    703             int total = 0;
    704             Atlas atlas = new Atlas(type, width, height);
    705 
    706             final int count = mBitmaps.size();
    707             for (int i = 0; i < count; i++) {
    708                 final Bitmap bitmap = mBitmaps.get(i);
    709                 if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) {
    710                     total++;
    711                 }
    712             }
    713 
    714             return total;
    715         }
    716     }
    717 }
    718