| /* | |
| * 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; | |
| } | |
| } | |
| } |