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