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