AssetAtlasService.java revision dad7d84c04c5954b63ea8bb58c52b2291f44b4df
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 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.Arrays; 50import java.util.Collection; 51import java.util.Collections; 52import java.util.Comparator; 53import java.util.HashSet; 54import 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 72 private static final String LOG_TAG = "AssetAtlas"; 73 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: 120 // 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; 125 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 137 Collection<Bitmap> bitmaps = new HashSet<Bitmap>(300); 138 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++) { 146 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; 151 } 152 } 153 154 ArrayList<Bitmap> sortedBitmaps = new ArrayList<Bitmap>(bitmaps); 155 // Our algorithms perform better when the bitmaps are first sorted 156 // The comparator will sort the bitmap by width first, then by height 157 Collections.sort(sortedBitmaps, new Comparator<Bitmap>() { 158 @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 168 new Thread(new Renderer(sortedBitmaps, totalPixelCount)).start(); 169 } 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 */ 193 public void systemRunning() { 194 } 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 203 private long mNativeBitmap; 204 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 267 mAtlasMap = new long[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT]; 268 long[] atlasMap = mAtlasMap; 269 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(); 295 atlasMap[mapIndex++] = bitmap.mNativeBitmap; 296 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 371 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); 374 375 @Override 376 public boolean isCompatible(int ppid) { 377 return ppid == android.os.Process.myPpid(); 378 } 379 380 @Override 381 public GraphicBuffer getBuffer() throws RemoteException { 382 return mAtlasReady.get() ? mBuffer : null; 383 } 384 385 @Override 386 public long[] getMap() throws RemoteException { 387 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} 739