From 7c0cd6379c23d5a9cdf8893ad993edce3feda064 Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Mon, 20 Nov 2017 22:14:19 -0800 Subject: [PATCH] Optimize loops in StandardGifDecoder. These small additive changes might save ~5-10% decoding some GIFs, depending on the vagaries of the particular file. Progress towards #2471. --- .../glide/gifdecoder/StandardGifDecoder.java | 274 +++++++++++------- 1 file changed, 173 insertions(+), 101 deletions(-) diff --git a/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java b/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java index 01ee55c8b3..e50d64398b 100644 --- a/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java +++ b/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java @@ -32,6 +32,7 @@ import android.graphics.Bitmap.Config; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -100,8 +101,8 @@ public class StandardGifDecoder implements GifDecoder { * Reads 16k chunks from the native buffer for processing, to greatly reduce JNI overhead. */ private byte[] workBuffer; - private int workBufferSize = 0; - private int workBufferPosition = 0; + private int workBufferSize; + private int workBufferPosition; private GifHeaderParser parser; @@ -122,7 +123,8 @@ public class StandardGifDecoder implements GifDecoder { private int sampleSize; private int downsampledHeight; private int downsampledWidth; - private boolean isFirstFrameTransparent; + @Nullable + private Boolean isFirstFrameTransparent; @NonNull private Bitmap.Config bitmapConfig = Config.ARGB_8888; @@ -253,6 +255,13 @@ public synchronized Bitmap getNextFrame() { } status = STATUS_OK; + if (block == null) { + block = bitmapProvider.obtainByteArray(255); + } + if (workBuffer == null) { + workBuffer = bitmapProvider.obtainByteArray(WORK_BUFFER_SIZE); + } + GifFrame currentFrame = header.frames.get(framePointer); GifFrame previousFrame = null; int previousIndex = framePointer - 1; @@ -331,7 +340,7 @@ public void clear() { } previousImage = null; rawData = null; - isFirstFrameTransparent = false; + isFirstFrameTransparent = null; if (block != null) { bitmapProvider.release(block); } @@ -359,7 +368,6 @@ public synchronized void setData(GifHeader header, ByteBuffer buffer, int sample sampleSize = Integer.highestOneBit(sampleSize); this.status = STATUS_OK; this.header = header; - isFirstFrameTransparent = false; framePointer = INITIAL_FRAME_POINTER; // Initialize the raw data buffer. rawData = buffer.asReadOnlyBuffer(); @@ -478,6 +486,85 @@ private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { // Decode pixels for this frame into the global pixels[] scratch. decodeBitmapData(currentFrame); + if (currentFrame.interlace || sampleSize != 1) { + copyCopyIntoScratchRobust(currentFrame); + } else { + copyIntoScratchFast(currentFrame); + } + + // Copy pixels into previous image + if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED + || currentFrame.dispose == DISPOSAL_NONE)) { + if (previousImage == null) { + previousImage = getNextBitmap(); + } + previousImage.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, + downsampledHeight); + } + + // Set pixels for current image. + Bitmap result = getNextBitmap(); + result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight); + return result; + } + + private void copyIntoScratchFast(GifFrame currentFrame) { + int[] dest = mainScratch; + int downsampledIH = currentFrame.ih; + int downsampledIY = currentFrame.iy; + int downsampledIW = currentFrame.iw; + int downsampledIX = currentFrame.ix; + // Copy each source line to the appropriate place in the destination. + boolean isFirstFrame = framePointer == 0; + int width = this.downsampledWidth; + byte[] mainPixels = this.mainPixels; + int[] act = this.act; + @Nullable Boolean isFirstFrameTransparent = this.isFirstFrameTransparent; + for (int i = 0; i < downsampledIH; i++) { + int line = i + downsampledIY; + int k = line * width; + // Start of line in dest. + int dx = k + downsampledIX; + // End of dest line. + int dlim = dx + downsampledIW; + if (k + width < dlim) { + // Past dest edge. + dlim = k + width; + } + // Start of line in source. + int sx = i * currentFrame.iw; + int averageColor; + if (isFirstFrameTransparent == null && isFirstFrame) { + while (dx < dlim) { + int currentColorIndex = ((int) mainPixels[sx]) & MASK_INT_LOWEST_BYTE; + averageColor = act[currentColorIndex]; + if (averageColor != COLOR_TRANSPARENT_BLACK) { + dest[dx] = averageColor; + } else if (isFirstFrameTransparent == null) { + isFirstFrameTransparent = true; + } + ++sx; + ++dx; + } + } else { + while (dx < dlim) { + int currentColorIndex = ((int) mainPixels[sx]) & MASK_INT_LOWEST_BYTE; + averageColor = act[currentColorIndex]; + if (averageColor != COLOR_TRANSPARENT_BLACK) { + dest[dx] = averageColor; + } + ++sx; + ++dx; + } + } + } + + this.isFirstFrameTransparent = isFirstFrameTransparent == null + ? false : isFirstFrameTransparent; + } + + private void copyCopyIntoScratchRobust(GifFrame currentFrame) { + int[] dest = mainScratch; int downsampledIH = currentFrame.ih / sampleSize; int downsampledIY = currentFrame.iy / sampleSize; int downsampledIW = currentFrame.iw / sampleSize; @@ -492,7 +579,8 @@ private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { int downsampledHeight = this.downsampledHeight; byte[] mainPixels = this.mainPixels; int[] act = this.act; - boolean isFirstFrameTransparent = false; + @Nullable + Boolean isFirstFrameTransparent = this.isFirstFrameTransparent; for (int i = 0; i < downsampledIH; i++) { int line = i; if (currentFrame.interlace) { @@ -518,6 +606,7 @@ private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { iline += inc; } line += downsampledIY; + boolean isNotDownsampling = sampleSize == 1; if (line < downsampledHeight) { int k = line * downsampledWidth; // Start of line in dest. @@ -530,47 +619,47 @@ private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { } // Start of line in source. int sx = i * sampleSize * currentFrame.iw; - int maxPositionInSource = sx + ((dlim - dx) * sampleSize); - while (dx < dlim) { - // Map color and insert in destination. - @ColorInt int averageColor; - if (sampleSize == 1) { + if (isNotDownsampling) { + int averageColor; + while (dx < dlim) { int currentColorIndex = ((int) mainPixels[sx]) & MASK_INT_LOWEST_BYTE; averageColor = act[currentColorIndex]; - } else { + if (averageColor != COLOR_TRANSPARENT_BLACK) { + dest[dx] = averageColor; + } else if ( + isFirstFrameTransparent == null && isFirstFrame && !isFirstFrameTransparent) { + isFirstFrameTransparent = true; + } + sx += sampleSize; + dx++; + } + } else { + int averageColor; + int maxPositionInSource = sx + ((dlim - dx) * sampleSize); + while (dx < dlim) { + // Map color and insert in destination. // TODO: This is substantially slower (up to 50ms per frame) than just grabbing the // current color index above, even with a sample size of 1. averageColor = averageColorsNear(sx, maxPositionInSource, currentFrame.iw); + if (averageColor != COLOR_TRANSPARENT_BLACK) { + dest[dx] = averageColor; + } else if (isFirstFrame && !isFirstFrameTransparent) { + isFirstFrameTransparent = true; + } + sx += sampleSize; + dx++; } - if (averageColor != COLOR_TRANSPARENT_BLACK) { - dest[dx] = averageColor; - } else if (isFirstFrame && !isFirstFrameTransparent) { - isFirstFrameTransparent = true; - } - sx += sampleSize; - dx++; } } } - this.isFirstFrameTransparent = isFirstFrameTransparent; - - // Copy pixels into previous image - if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED - || currentFrame.dispose == DISPOSAL_NONE)) { - if (previousImage == null) { - previousImage = getNextBitmap(); - } - previousImage.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, - downsampledHeight); + if (this.isFirstFrameTransparent == null) { + this.isFirstFrameTransparent = isFirstFrameTransparent == null + ? false : isFirstFrameTransparent; } - - // Set pixels for current image. - Bitmap result = getNextBitmap(); - result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight); - return result; } + @ColorInt private int averageColorsNear(int positionInMainPixels, int maxPositionInMainPixels, int currentFrameIw) { @@ -659,32 +748,30 @@ private void decodeBitmapData(GifFrame frame) { oldCode = NULL_CODE; codeSize = dataSize + 1; codeMask = (1 << codeSize) - 1; + for (code = 0; code < clear; code++) { // XXX ArrayIndexOutOfBoundsException. prefix[code] = 0; suffix[code] = (byte) code; } - byte[] block = this.block; // Decode GIF pixel stream. - datum = bits = count = first = top = pi = bi = 0; - for (i = 0; i < npix; ) { - // Load bytes until there are enough bits for a code. - if (count == 0) { + i = datum = bits = count = first = top = pi = bi = 0; + while (i < npix) { // Read a new data block. + if (count == 0) { count = readBlock(); if (count <= 0) { status = STATUS_PARTIAL_DECODE; break; } bi = 0; - block = this.block; } datum += (((int) block[bi]) & MASK_INT_LOWEST_BYTE) << bits; bits += 8; - bi++; - count--; + ++bi; + --count; while (bits >= codeSize) { // Get the next code. @@ -700,52 +787,52 @@ private void decodeBitmapData(GifFrame frame) { available = clear + 2; oldCode = NULL_CODE; continue; - } - - if (code > available) { - status = STATUS_PARTIAL_DECODE; + } else if (code == endOfInformation) { break; - } - - if (code == endOfInformation) { - break; - } - - if (oldCode == NULL_CODE) { - pixelStack[top++] = suffix[code]; + } else if (oldCode == NULL_CODE) { + pixelStack[top] = suffix[code]; + ++top; oldCode = code; first = code; continue; } + inCode = code; if (code >= available) { - pixelStack[top++] = (byte) first; + pixelStack[top] = (byte) first; + ++top; code = oldCode; } + while (code >= clear) { - pixelStack[top++] = suffix[code]; + pixelStack[top] = suffix[code]; + ++top; code = prefix[code]; } first = ((int) suffix[code]) & MASK_INT_LOWEST_BYTE; - pixelStack[top++] = (byte) first; + + mainPixels[pi] = (byte) first; + ++pi; + ++i; + + while (top > 0) { + // Pop a pixel off the pixel stack. + mainPixels[pi] = pixelStack[--top]; + ++pi; + ++i; + } // Add a new string to the string table. if (available < MAX_STACK_SIZE) { prefix[available] = (short) oldCode; suffix[available] = (byte) first; - available++; + ++available; if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) { - codeSize++; + ++codeSize; codeMask += available; } } oldCode = inCode; - - while (top > 0) { - // Pop a pixel off the pixel stack. - mainPixels[pi++] = pixelStack[--top]; - i++; - } } } @@ -760,9 +847,6 @@ private void readChunkIfNeeded() { if (workBufferSize > workBufferPosition) { return; } - if (workBuffer == null) { - workBuffer = bitmapProvider.obtainByteArray(WORK_BUFFER_SIZE); - } workBufferPosition = 0; workBufferSize = Math.min(rawData.remaining(), WORK_BUFFER_SIZE); rawData.get(workBuffer, 0, workBufferSize); @@ -772,13 +856,8 @@ private void readChunkIfNeeded() { * Reads a single byte from the input stream. */ private int readByte() { - try { - readChunkIfNeeded(); - return workBuffer[workBufferPosition++] & MASK_INT_LOWEST_BYTE; - } catch (Exception e) { - status = STATUS_FORMAT_ERROR; - return 0; - } + readChunkIfNeeded(); + return workBuffer[workBufferPosition++] & MASK_INT_LOWEST_BYTE; } /** @@ -788,37 +867,30 @@ private int readByte() { */ private int readBlock() { int blockSize = readByte(); - if (blockSize > 0) { - try { - if (block == null) { - block = bitmapProvider.obtainByteArray(255); - } - final int remaining = workBufferSize - workBufferPosition; - if (remaining >= blockSize) { - // Block can be read from the current work buffer. - System.arraycopy(workBuffer, workBufferPosition, block, 0, blockSize); - workBufferPosition += blockSize; - } else if (rawData.remaining() + remaining >= blockSize) { - // Block can be read in two passes. - System.arraycopy(workBuffer, workBufferPosition, block, 0, remaining); - workBufferPosition = workBufferSize; - readChunkIfNeeded(); - final int secondHalfRemaining = blockSize - remaining; - System.arraycopy(workBuffer, 0, block, remaining, secondHalfRemaining); - workBufferPosition += secondHalfRemaining; - } else { - status = STATUS_FORMAT_ERROR; - } - } catch (Exception e) { - Log.w(TAG, "Error Reading Block", e); - status = STATUS_FORMAT_ERROR; - } + if (blockSize <= 0) { + return blockSize; + } + final int remaining = workBufferSize - workBufferPosition; + if (remaining >= blockSize) { + // Block can be read from the current work buffer. + System.arraycopy(workBuffer, workBufferPosition, block, 0, blockSize); + workBufferPosition += blockSize; + } else if (rawData.remaining() + remaining >= blockSize) { + // Block can be read in two passes. + System.arraycopy(workBuffer, workBufferPosition, block, 0, remaining); + workBufferPosition = workBufferSize; + readChunkIfNeeded(); + final int secondHalfRemaining = blockSize - remaining; + System.arraycopy(workBuffer, 0, block, remaining, secondHalfRemaining); + workBufferPosition += secondHalfRemaining; + } else { + status = STATUS_FORMAT_ERROR; } return blockSize; } private Bitmap getNextBitmap() { - Bitmap.Config config = isFirstFrameTransparent + Bitmap.Config config = isFirstFrameTransparent == null || isFirstFrameTransparent ? Bitmap.Config.ARGB_8888 : bitmapConfig; Bitmap result = bitmapProvider.obtain(downsampledWidth, downsampledHeight, config); result.setHasAlpha(true);