summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/AssetAtlasService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/AssetAtlasService.java')
-rw-r--r--services/core/java/com/android/server/AssetAtlasService.java735
1 files changed, 735 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/AssetAtlasService.java b/services/core/java/com/android/server/AssetAtlasService.java
new file mode 100644
index 0000000..26b4652
--- /dev/null
+++ b/services/core/java/com/android/server/AssetAtlasService.java
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Atlas;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.drawable.Drawable;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.view.GraphicBuffer;
+import android.view.IAssetAtlas;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This service is responsible for packing preloaded bitmaps into a single
+ * atlas texture. The resulting texture can be shared across processes to
+ * reduce overall memory usage.
+ *
+ * @hide
+ */
+public class AssetAtlasService extends IAssetAtlas.Stub {
+ /**
+ * Name of the <code>AssetAtlasService</code>.
+ */
+ public static final String ASSET_ATLAS_SERVICE = "assetatlas";
+
+ private static final String LOG_TAG = "Atlas";
+
+ // Turns debug logs on/off. Debug logs are kept to a minimum and should
+ // remain on to diagnose issues
+ private static final boolean DEBUG_ATLAS = true;
+
+ // When set to true the content of the atlas will be saved to disk
+ // in /data/system/atlas.png. The shared GraphicBuffer may be empty
+ private static final boolean DEBUG_ATLAS_TEXTURE = false;
+
+ // Minimum size in pixels to consider for the resulting texture
+ private static final int MIN_SIZE = 768;
+ // Maximum size in pixels to consider for the resulting texture
+ private static final int MAX_SIZE = 2048;
+ // Increment in number of pixels between size variants when looking
+ // for the best texture dimensions
+ private static final int STEP = 64;
+
+ // This percentage of the total number of pixels represents the minimum
+ // number of pixels we want to be able to pack in the atlas
+ private static final float PACKING_THRESHOLD = 0.8f;
+
+ // Defines the number of int fields used to represent a single entry
+ // in the atlas map. This number defines the size of the array returned
+ // by the getMap(). See the mAtlasMap field for more information
+ private static final int ATLAS_MAP_ENTRY_FIELD_COUNT = 4;
+
+ // Specifies how our GraphicBuffer will be used. To get proper swizzling
+ // the buffer will be written to using OpenGL (from JNI) so we can leave
+ // the software flag set to "never"
+ private static final int GRAPHIC_BUFFER_USAGE = GraphicBuffer.USAGE_SW_READ_NEVER |
+ GraphicBuffer.USAGE_SW_WRITE_NEVER | GraphicBuffer.USAGE_HW_TEXTURE;
+
+ // This boolean is set to true if an atlas was successfully
+ // computed and rendered
+ private final AtomicBoolean mAtlasReady = new AtomicBoolean(false);
+
+ private final Context mContext;
+
+ // Version name of the current build, used to identify changes to assets list
+ private final String mVersionName;
+
+ // Holds the atlas' data. This buffer can be mapped to
+ // OpenGL using an EGLImage
+ private GraphicBuffer mBuffer;
+
+ // Describes how bitmaps are placed in the atlas. Each bitmap is
+ // represented by several entries in the array:
+ // int0: SkBitmap*, the native bitmap object
+ // int1: x position
+ // int2: y position
+ // int3: rotated, 1 if the bitmap must be rotated, 0 otherwise
+ // NOTE: This will need to be handled differently to support 64 bit pointers
+ private int[] mAtlasMap;
+
+ /**
+ * Creates a new service. Upon creating, the service will gather the list of
+ * assets to consider for packing into the atlas and spawn a new thread to
+ * start the packing work.
+ *
+ * @param context The context giving access to preloaded resources
+ */
+ public AssetAtlasService(Context context) {
+ mContext = context;
+ mVersionName = queryVersionName(context);
+
+ ArrayList<Bitmap> bitmaps = new ArrayList<Bitmap>(300);
+ int totalPixelCount = 0;
+
+ // We only care about drawables that hold bitmaps
+ final Resources resources = context.getResources();
+ final LongSparseArray<Drawable.ConstantState> drawables = resources.getPreloadedDrawables();
+
+ final int count = drawables.size();
+ for (int i = 0; i < count; i++) {
+ final Bitmap bitmap = drawables.valueAt(i).getBitmap();
+ if (bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888) {
+ bitmaps.add(bitmap);
+ totalPixelCount += bitmap.getWidth() * bitmap.getHeight();
+ }
+ }
+
+ // Our algorithms perform better when the bitmaps are first sorted
+ // The comparator will sort the bitmap by width first, then by height
+ Collections.sort(bitmaps, new Comparator<Bitmap>() {
+ @Override
+ public int compare(Bitmap b1, Bitmap b2) {
+ if (b1.getWidth() == b2.getWidth()) {
+ return b2.getHeight() - b1.getHeight();
+ }
+ return b2.getWidth() - b1.getWidth();
+ }
+ });
+
+ // Kick off the packing work on a worker thread
+ new Thread(new Renderer(bitmaps, totalPixelCount)).start();
+ }
+
+ /**
+ * Queries the version name stored in framework's AndroidManifest.
+ * The version name can be used to identify possible changes to
+ * framework resources.
+ *
+ * @see #getBuildIdentifier(String)
+ */
+ private static String queryVersionName(Context context) {
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ return info.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(LOG_TAG, "Could not get package info", e);
+ }
+ return null;
+ }
+
+ /**
+ * Callback invoked by the server thread to indicate we can now run
+ * 3rd party code.
+ */
+ public void systemRunning() {
+ }
+
+ /**
+ * The renderer does all the work:
+ */
+ private class Renderer implements Runnable {
+ private final ArrayList<Bitmap> mBitmaps;
+ private final int mPixelCount;
+
+ private int mNativeBitmap;
+
+ // Used for debugging only
+ private Bitmap mAtlasBitmap;
+
+ Renderer(ArrayList<Bitmap> bitmaps, int pixelCount) {
+ mBitmaps = bitmaps;
+ mPixelCount = pixelCount;
+ }
+
+ /**
+ * 1. On first boot or after every update, brute-force through all the
+ * possible atlas configurations and look for the best one (maximimize
+ * number of packed assets and minimize texture size)
+ * a. If a best configuration was computed, write it out to disk for
+ * future use
+ * 2. Read best configuration from disk
+ * 3. Compute the packing using the best configuration
+ * 4. Allocate a GraphicBuffer
+ * 5. Render assets in the buffer
+ */
+ @Override
+ public void run() {
+ Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName);
+ if (DEBUG_ATLAS) Log.d(LOG_TAG, "Loaded configuration: " + config);
+
+ if (config != null) {
+ mBuffer = GraphicBuffer.create(config.width, config.height,
+ PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE);
+
+ if (mBuffer != null) {
+ Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags);
+ if (renderAtlas(mBuffer, atlas, config.count)) {
+ mAtlasReady.set(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Renders a list of bitmaps into the atlas. The position of each bitmap
+ * was decided by the packing algorithm and will be honored by this
+ * method. If need be this method will also rotate bitmaps.
+ *
+ * @param buffer The buffer to render the atlas entries into
+ * @param atlas The atlas to pack the bitmaps into
+ * @param packCount The number of bitmaps that will be packed in the atlas
+ *
+ * @return true if the atlas was rendered, false otherwise
+ */
+ @SuppressWarnings("MismatchedReadAndWriteOfArray")
+ private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) {
+ // Use a Source blend mode to improve performance, the target bitmap
+ // will be zero'd out so there's no need to waste time applying blending
+ final Paint paint = new Paint();
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+
+ // We always render the atlas into a bitmap. This bitmap is then
+ // uploaded into the GraphicBuffer using OpenGL to swizzle the content
+ final Canvas canvas = acquireCanvas(buffer.getWidth(), buffer.getHeight());
+ if (canvas == null) return false;
+
+ final Atlas.Entry entry = new Atlas.Entry();
+
+ mAtlasMap = new int[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT];
+ int[] atlasMap = mAtlasMap;
+ int mapIndex = 0;
+
+ boolean result = false;
+ try {
+ final long startRender = System.nanoTime();
+ final int count = mBitmaps.size();
+
+ for (int i = 0; i < count; i++) {
+ final Bitmap bitmap = mBitmaps.get(i);
+ if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) {
+ // We have more bitmaps to pack than the current configuration
+ // says, we were most likely not able to detect a change in the
+ // list of preloaded drawables, abort and delete the configuration
+ if (mapIndex >= mAtlasMap.length) {
+ deleteDataFile();
+ break;
+ }
+
+ canvas.save();
+ canvas.translate(entry.x, entry.y);
+ if (entry.rotated) {
+ canvas.translate(bitmap.getHeight(), 0.0f);
+ canvas.rotate(90.0f);
+ }
+ canvas.drawBitmap(bitmap, 0.0f, 0.0f, null);
+ canvas.restore();
+
+ atlasMap[mapIndex++] = bitmap.mNativeBitmap;
+ atlasMap[mapIndex++] = entry.x;
+ atlasMap[mapIndex++] = entry.y;
+ atlasMap[mapIndex++] = entry.rotated ? 1 : 0;
+ }
+ }
+
+ final long endRender = System.nanoTime();
+ if (mNativeBitmap != 0) {
+ result = nUploadAtlas(buffer, mNativeBitmap);
+ }
+
+ final long endUpload = System.nanoTime();
+ if (DEBUG_ATLAS) {
+ float renderDuration = (endRender - startRender) / 1000.0f / 1000.0f;
+ float uploadDuration = (endUpload - endRender) / 1000.0f / 1000.0f;
+ Log.d(LOG_TAG, String.format("Rendered atlas in %.2fms (%.2f+%.2fms)",
+ renderDuration + uploadDuration, renderDuration, uploadDuration));
+ }
+
+ } finally {
+ releaseCanvas(canvas);
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a Canvas for the specified buffer. If {@link #DEBUG_ATLAS_TEXTURE}
+ * is turned on, the returned Canvas will render into a local bitmap that
+ * will then be saved out to disk for debugging purposes.
+ * @param width
+ * @param height
+ */
+ private Canvas acquireCanvas(int width, int height) {
+ if (DEBUG_ATLAS_TEXTURE) {
+ mAtlasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ return new Canvas(mAtlasBitmap);
+ } else {
+ Canvas canvas = new Canvas();
+ mNativeBitmap = nAcquireAtlasCanvas(canvas, width, height);
+ return canvas;
+ }
+ }
+
+ /**
+ * Releases the canvas used to render into the buffer. Calling this method
+ * will release any resource previously acquired. If {@link #DEBUG_ATLAS_TEXTURE}
+ * is turend on, calling this method will write the content of the atlas
+ * to disk in /data/system/atlas.png for debugging.
+ */
+ private void releaseCanvas(Canvas canvas) {
+ if (DEBUG_ATLAS_TEXTURE) {
+ canvas.setBitmap(null);
+
+ File systemDirectory = new File(Environment.getDataDirectory(), "system");
+ File dataFile = new File(systemDirectory, "atlas.png");
+
+ try {
+ FileOutputStream out = new FileOutputStream(dataFile);
+ mAtlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.close();
+ } catch (FileNotFoundException e) {
+ // Ignore
+ } catch (IOException e) {
+ // Ignore
+ }
+
+ mAtlasBitmap.recycle();
+ mAtlasBitmap = null;
+ } else {
+ nReleaseAtlasCanvas(canvas, mNativeBitmap);
+ }
+ }
+ }
+
+ private static native int nAcquireAtlasCanvas(Canvas canvas, int width, int height);
+ private static native void nReleaseAtlasCanvas(Canvas canvas, int bitmap);
+ private static native boolean nUploadAtlas(GraphicBuffer buffer, int bitmap);
+
+ @Override
+ public boolean isCompatible(int ppid) {
+ return ppid == android.os.Process.myPpid();
+ }
+
+ @Override
+ public GraphicBuffer getBuffer() throws RemoteException {
+ return mAtlasReady.get() ? mBuffer : null;
+ }
+
+ @Override
+ public int[] getMap() throws RemoteException {
+ return mAtlasReady.get() ? mAtlasMap : null;
+ }
+
+ /**
+ * Finds the best atlas configuration to pack the list of supplied bitmaps.
+ * This method takes advantage of multi-core systems by spawning a number
+ * of threads equal to the number of available cores.
+ */
+ private static Configuration computeBestConfiguration(
+ ArrayList<Bitmap> bitmaps, int pixelCount) {
+ if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration...");
+
+ long begin = System.nanoTime();
+ List<WorkerResult> results = Collections.synchronizedList(new ArrayList<WorkerResult>());
+
+ // Don't bother with an extra thread if there's only one processor
+ int cpuCount = Runtime.getRuntime().availableProcessors();
+ if (cpuCount == 1) {
+ new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run();
+ } else {
+ int start = MIN_SIZE;
+ int end = MAX_SIZE - (cpuCount - 1) * STEP;
+ int step = STEP * cpuCount;
+
+ final CountDownLatch signal = new CountDownLatch(cpuCount);
+
+ for (int i = 0; i < cpuCount; i++, start += STEP, end += STEP) {
+ ComputeWorker worker = new ComputeWorker(start, end, step,
+ bitmaps, pixelCount, results, signal);
+ new Thread(worker, "Atlas Worker #" + (i + 1)).start();
+ }
+
+ try {
+ signal.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.w(LOG_TAG, "Could not complete configuration computation");
+ return null;
+ }
+ }
+
+ // Maximize the number of packed bitmaps, minimize the texture size
+ Collections.sort(results, new Comparator<WorkerResult>() {
+ @Override
+ public int compare(WorkerResult r1, WorkerResult r2) {
+ int delta = r2.count - r1.count;
+ if (delta != 0) return delta;
+ return r1.width * r1.height - r2.width * r2.height;
+ }
+ });
+
+ if (DEBUG_ATLAS) {
+ float delay = (System.nanoTime() - begin) / 1000.0f / 1000.0f / 1000.0f;
+ Log.d(LOG_TAG, String.format("Found best atlas configuration in %.2fs", delay));
+ }
+
+ WorkerResult result = results.get(0);
+ return new Configuration(result.type, result.width, result.height, result.count);
+ }
+
+ /**
+ * Returns the path to the file containing the best computed
+ * atlas configuration.
+ */
+ private static File getDataFile() {
+ File systemDirectory = new File(Environment.getDataDirectory(), "system");
+ return new File(systemDirectory, "framework_atlas.config");
+ }
+
+ private static void deleteDataFile() {
+ Log.w(LOG_TAG, "Current configuration inconsistent with assets list");
+ if (!getDataFile().delete()) {
+ Log.w(LOG_TAG, "Could not delete the current configuration");
+ }
+ }
+
+ private File getFrameworkResourcesFile() {
+ return new File(mContext.getApplicationInfo().sourceDir);
+ }
+
+ /**
+ * Returns the best known atlas configuration. This method will either
+ * read the configuration from disk or start a brute-force search
+ * and save the result out to disk.
+ */
+ private Configuration chooseConfiguration(ArrayList<Bitmap> bitmaps, int pixelCount,
+ String versionName) {
+ Configuration config = null;
+
+ final File dataFile = getDataFile();
+ if (dataFile.exists()) {
+ config = readConfiguration(dataFile, versionName);
+ }
+
+ if (config == null) {
+ config = computeBestConfiguration(bitmaps, pixelCount);
+ if (config != null) writeConfiguration(config, dataFile, versionName);
+ }
+
+ return config;
+ }
+
+ /**
+ * Writes the specified atlas configuration to the specified file.
+ */
+ private void writeConfiguration(Configuration config, File file, String versionName) {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
+ writer.write(getBuildIdentifier(versionName));
+ writer.newLine();
+ writer.write(config.type.toString());
+ writer.newLine();
+ writer.write(String.valueOf(config.width));
+ writer.newLine();
+ writer.write(String.valueOf(config.height));
+ writer.newLine();
+ writer.write(String.valueOf(config.count));
+ writer.newLine();
+ writer.write(String.valueOf(config.flags));
+ writer.newLine();
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "Could not write " + file, e);
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Could not write " + file, e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads an atlas configuration from the specified file. This method
+ * returns null if an error occurs or if the configuration is invalid.
+ */
+ private Configuration readConfiguration(File file, String versionName) {
+ BufferedReader reader = null;
+ Configuration config = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
+
+ if (checkBuildIdentifier(reader, versionName)) {
+ Atlas.Type type = Atlas.Type.valueOf(reader.readLine());
+ int width = readInt(reader, MIN_SIZE, MAX_SIZE);
+ int height = readInt(reader, MIN_SIZE, MAX_SIZE);
+ int count = readInt(reader, 0, Integer.MAX_VALUE);
+ int flags = readInt(reader, Integer.MIN_VALUE, Integer.MAX_VALUE);
+
+ config = new Configuration(type, width, height, count, flags);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.w(LOG_TAG, "Invalid parameter value in " + file, e);
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "Could not read " + file, e);
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Could not read " + file, e);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+ return config;
+ }
+
+ private static int readInt(BufferedReader reader, int min, int max) throws IOException {
+ return Math.max(min, Math.min(max, Integer.parseInt(reader.readLine())));
+ }
+
+ /**
+ * Compares the next line in the specified buffered reader to the current
+ * build identifier. Returns whether the two values are equal.
+ *
+ * @see #getBuildIdentifier(String)
+ */
+ private boolean checkBuildIdentifier(BufferedReader reader, String versionName)
+ throws IOException {
+ String deviceBuildId = getBuildIdentifier(versionName);
+ String buildId = reader.readLine();
+ return deviceBuildId.equals(buildId);
+ }
+
+ /**
+ * Returns an identifier for the current build that can be used to detect
+ * likely changes to framework resources. The build identifier is made of
+ * several distinct values:
+ *
+ * build fingerprint/framework version name/file size of framework resources apk
+ *
+ * Only the build fingerprint should be necessary on user builds but
+ * the other values are useful to detect changes on eng builds during
+ * development.
+ *
+ * This identifier does not attempt to be exact: a new identifier does not
+ * necessarily mean the preloaded drawables have changed. It is important
+ * however that whenever the list of preloaded drawables changes, this
+ * identifier changes as well.
+ *
+ * @see #checkBuildIdentifier(java.io.BufferedReader, String)
+ */
+ private String getBuildIdentifier(String versionName) {
+ return SystemProperties.get("ro.build.fingerprint", "") + '/' + versionName + '/' +
+ String.valueOf(getFrameworkResourcesFile().length());
+ }
+
+ /**
+ * Atlas configuration. Specifies the algorithm, dimensions and flags to use.
+ */
+ private static class Configuration {
+ final Atlas.Type type;
+ final int width;
+ final int height;
+ final int count;
+ final int flags;
+
+ Configuration(Atlas.Type type, int width, int height, int count) {
+ this(type, width, height, count, Atlas.FLAG_DEFAULTS);
+ }
+
+ Configuration(Atlas.Type type, int width, int height, int count, int flags) {
+ this.type = type;
+ this.width = width;
+ this.height = height;
+ this.count = count;
+ this.flags = flags;
+ }
+
+ @Override
+ public String toString() {
+ return type.toString() + " (" + width + "x" + height + ") flags=0x" +
+ Integer.toHexString(flags) + " count=" + count;
+ }
+ }
+
+ /**
+ * Used during the brute-force search to gather information about each
+ * variant of the packing algorithm.
+ */
+ private static class WorkerResult {
+ Atlas.Type type;
+ int width;
+ int height;
+ int count;
+
+ WorkerResult(Atlas.Type type, int width, int height, int count) {
+ this.type = type;
+ this.width = width;
+ this.height = height;
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %dx%d", type.toString(), width, height);
+ }
+ }
+
+ /**
+ * A compute worker will try a finite number of variations of the packing
+ * algorithms and save the results in a supplied list.
+ */
+ private static class ComputeWorker implements Runnable {
+ private final int mStart;
+ private final int mEnd;
+ private final int mStep;
+ private final List<Bitmap> mBitmaps;
+ private final List<WorkerResult> mResults;
+ private final CountDownLatch mSignal;
+ private final int mThreshold;
+
+ /**
+ * Creates a new compute worker to brute-force through a range of
+ * packing algorithms variants.
+ *
+ * @param start The minimum texture width to try
+ * @param end The maximum texture width to try
+ * @param step The number of pixels to increment the texture width by at each step
+ * @param bitmaps The list of bitmaps to pack in the atlas
+ * @param pixelCount The total number of pixels occupied by the list of bitmaps
+ * @param results The list of results in which to save the brute-force search results
+ * @param signal Latch to decrement when this worker is done, may be null
+ */
+ ComputeWorker(int start, int end, int step, List<Bitmap> bitmaps, int pixelCount,
+ List<WorkerResult> results, CountDownLatch signal) {
+ mStart = start;
+ mEnd = end;
+ mStep = step;
+ mBitmaps = bitmaps;
+ mResults = results;
+ mSignal = signal;
+
+ // Minimum number of pixels we want to be able to pack
+ int threshold = (int) (pixelCount * PACKING_THRESHOLD);
+ // Make sure we can find at least one configuration
+ while (threshold > MAX_SIZE * MAX_SIZE) {
+ threshold >>= 1;
+ }
+ mThreshold = threshold;
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName());
+
+ Atlas.Entry entry = new Atlas.Entry();
+ for (Atlas.Type type : Atlas.Type.values()) {
+ for (int width = mStart; width < mEnd; width += mStep) {
+ for (int height = MIN_SIZE; height < MAX_SIZE; height += STEP) {
+ // If the atlas is not big enough, skip it
+ if (width * height <= mThreshold) continue;
+
+ final int count = packBitmaps(type, width, height, entry);
+ if (count > 0) {
+ mResults.add(new WorkerResult(type, width, height, count));
+ // If we were able to pack everything let's stop here
+ // Increasing the height further won't make things better
+ if (count == mBitmaps.size()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (mSignal != null) {
+ mSignal.countDown();
+ }
+ }
+
+ private int packBitmaps(Atlas.Type type, int width, int height, Atlas.Entry entry) {
+ int total = 0;
+ Atlas atlas = new Atlas(type, width, height);
+
+ final int count = mBitmaps.size();
+ for (int i = 0; i < count; i++) {
+ final Bitmap bitmap = mBitmaps.get(i);
+ if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) {
+ total++;
+ }
+ }
+
+ return total;
+ }
+ }
+}