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