解讀Android GIF文件native渲染之OpenGL

本系列文章以koral實現的GIF文件native渲染爲根據,解讀實現的大致過程以及關鍵代碼分析,github地址:https://github.com/koral–/android-gif-drawable
由前文中紋理貼圖native實現可知,若想實現圖像的OpenGL渲染(主要glTexImage2D函數),首先需要解析出該圖像文件(當時PNG爲例)的圖像數據以及其它相關信息,然後利用native OpenGL API實現,而GIF文件的渲染過程稍微麻煩一些,因爲涉及到多幀圖像的渲染,具體過程可以大致分爲以下幾個步驟。

(1)創建解析GIF文件的句柄

利用GIFLIB解析GIF文件,把相關信息保存在GifInfo結構體中,並將該結構體的指針(long)返回給JAVA層,作爲訪問GIF文件這些信息的句柄。從JAVA層到GIFLIB的大致實現流程如下:
這裏寫圖片描述
GifInfo結構體:

struct GifInfo {
    void (*destructor)(GifInfo *, JNIEnv *);
    GifFileType *gifFilePtr;
    GifWord originalWidth, originalHeight;
    uint_fast16_t sampleSize;
    long long lastFrameRemainder;
    long long nextStartTime;
    uint_fast32_t currentIndex;
    GraphicsControlBlock *controlBlock;
    argb *backupPtr;
    long long startPos;
    unsigned char *rasterBits;
    uint_fast32_t rasterSize;
    char *comment;
    uint_fast16_t loopCount;
    uint_fast16_t currentLoop;
    RewindFunc rewindFunction;
    jfloat speedFactor;
    int32_t stride;
    jlong sourceLength;
    bool isOpaque;
    void *frameBufferDescriptor;
};

解析GIF文件相關信息的關鍵代碼:

/******************************************************************************
This routine should be called before any other DGif calls. Note that
this routine is called automatically from DGif file open routines.
******************************************************************************/
int
DGifGetScreenDesc(GifFileType *GifFile) {
//    bool SortFlag;
    GifByteType Buf[3];
//    GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private;

//    if (!IS_READABLE(Private)) {
//        /* This file was NOT open for reading: */
//        GifFile->Error = D_GIF_ERR_NOT_READABLE;
//        return GIF_ERROR;
//    }

    /* Put the screen descriptor into the file: */
    if (DGifGetWord(GifFile, &GifFile->SWidth) == GIF_ERROR ||
        DGifGetWord(GifFile, &GifFile->SHeight) == GIF_ERROR)
        return GIF_ERROR;

    if (((GifFilePrivateType *) GifFile->Private)->Read(GifFile, Buf, 3) != 3) {
        GifFile->Error = D_GIF_ERR_READ_FAILED;
        GifFreeMapObject(GifFile->SColorMap);
        GifFile->SColorMap = NULL;
        return GIF_ERROR;
    }
//    GifFile->SColorResolution = (((Buf[0] & 0x70) + 1) >> 4) + 1;
//    SortFlag = (Buf[0] & 0x08) != 0;
    uint_fast8_t BitsPerPixel = (uint_fast8_t) ((Buf[0] & 0x07) + 1);
    GifFile->SBackGroundColor = Buf[1];
//    GifFile->AspectByte = Buf[2];
    if (Buf[0] & 0x80) {    /* Do we have global color map? */
        uint_fast16_t i;

        GifFile->SColorMap = GifMakeMapObject(BitsPerPixel, NULL);
        if (GifFile->SColorMap == NULL) {
            GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM;
            return GIF_ERROR;
        }

        /* Get the global color map: */
//  GifFile->SColorMap->SortFlag = SortFlag;
        for (i = 0; i < GifFile->SColorMap->ColorCount; i++) {
            if (((GifFilePrivateType *) GifFile->Private)->Read(GifFile, Buf, 3) != 3) {
                GifFreeMapObject(GifFile->SColorMap);
                GifFile->SColorMap = NULL;
                GifFile->Error = D_GIF_ERR_READ_FAILED;
                return GIF_ERROR;
            }
            GifFile->SColorMap->Colors[i].Red = Buf[0];
            GifFile->SColorMap->Colors[i].Green = Buf[1];
            GifFile->SColorMap->Colors[i].Blue = Buf[2];
        }
    } else {
        GifFile->SColorMap = NULL;
    }

    return GIF_OK;
}

(2)讀取每幀的圖像數據

由於GIF文件是多幀的,所以GIF文件的渲染需要根據每幀的持續時間來獲取不同幀圖像來渲染,大致流程如下圖:
這裏寫圖片描述

getBitmap用於獲取當前幀的圖像數據,並保存在GifInfo結構的frameBufferDescriptor所指向的TexImageDescriptor結構中frameBuffer。
TexImageDescriptor結構:

typedef struct {
    struct pollfd eventPollFd;
    void *frameBuffer;
    pthread_mutex_t renderMutex;
    pthread_t slurpThread;
} TexImageDescriptor;

關鍵代碼如下:

static void *slurp(void *pVoidInfo) {
    GifInfo *info = pVoidInfo;

    while (true) {
        long renderStartTime = getRealTime();
        // reset gif info
        DDGifSlurp(info, true, false);
        TexImageDescriptor *texImageDescriptor = info->frameBufferDescriptor;
        pthread_mutex_lock(&texImageDescriptor->renderMutex);
        if (info->currentIndex == 0) {
            prepareCanvas(texImageDescriptor->frameBuffer, info);
        }
        const uint_fast32_t frameDuration = getBitmap(texImageDescriptor->frameBuffer, info);
        pthread_mutex_unlock(&texImageDescriptor->renderMutex);

        const long long invalidationDelayMillis = calculateInvalidationDelay(info, renderStartTime, frameDuration);
        int pollResult = poll(&texImageDescriptor->eventPollFd, 1, (int) invalidationDelayMillis);
        DDGifSlurpTest(info, true, false);

        eventfd_t eventValue;
        if (pollResult < 0) {
            throwException(getEnv(), RUNTIME_EXCEPTION_ERRNO, "Could not poll on eventfd ");
            break;
        } else if (pollResult > 0) {
            const int readResult = TEMP_FAILURE_RETRY(eventfd_read(texImageDescriptor->eventPollFd.fd, &eventValue));
            if (readResult != 0) {
                throwException(getEnv(), RUNTIME_EXCEPTION_ERRNO, "Could not read from eventfd ");
            }
            break;
        }
    }
    DetachCurrentThread();
    return NULL;
}

(3)實現指定和替換紋理

在前面過程中,我們可以拿到GIF文件的相關信息以及當前所需繪製的幀圖像,這樣再實現紋理的指定和替換就很簡單了,代碼如下:
1.紋理指定

Java_pl_droidsonroids_gif_GifInfoHandle_glTexImage2D(JNIEnv *__unused unused, jclass __unused handleClass, jlong gifInfo, jint target, jint level) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL || info->frameBufferDescriptor == NULL) {
        return;
    }
    const GLsizei width = (const GLsizei) info->gifFilePtr->SWidth;
    const GLsizei height = (const GLsizei) info->gifFilePtr->SHeight;
    TexImageDescriptor *descriptor = info->frameBufferDescriptor;
    void *const pixels = descriptor->frameBuffer;
    pthread_mutex_lock(&descriptor->renderMutex);
    glTexImage2D((GLenum) target, level, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    pthread_mutex_unlock(&descriptor->renderMutex);
}

2、紋理替換或修改

Java_pl_droidsonroids_gif_GifInfoHandle_glTexSubImage2D(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo, jint target, jint level) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL || info->frameBufferDescriptor == NULL) {
        return;
    }
    const GLsizei width = (const GLsizei) info->gifFilePtr->SWidth;
    const GLsizei height = (const GLsizei) info->gifFilePtr->SHeight;
    TexImageDescriptor *descriptor = info->frameBufferDescriptor;
    void *const pixels = descriptor->frameBuffer;
    pthread_mutex_lock(&descriptor->renderMutex);
    glTexSubImage2D((GLenum) target, level, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    pthread_mutex_unlock(&descriptor->renderMutex);
}

由於整個實現過程都在native中,基本上不申請JAVA堆內存,因此不會出現OOM問題,但是顯然不適宜列表加載很多GIF文件,因爲頻繁申請和釋放native內存,會產生大量的內存碎片。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章