Skip to content

Caching compiled program binaries

Austin Kinross edited this page Mar 13, 2015 · 1 revision

Overview

  • Speeds up GL shader loading times.
  • Potentially allows apps to significantly reduce their startup time.

Introduction

Compiling OpenGL ES shaders and programs at runtime can be expensive, particularly for complicated shaders. This problem affects ANGLE even more than typical OpenGL ES drivers, due to the additional ESSL to HLSL translation that has to occur.

OpenGL ES developers recognized this issue many years ago and came up with the OES_get_program_binary extension, which ANGLE implements.


OES_get_program_binary

Instead of your app compiling its shaders every time it's run, the OES_get_program_binary extension allows your app to compile its shaders/programs once (e.g. during the app's first run) and save the compiled data to disk for later use using "glGetProgramBinaryOES". During subsequent runs of the app, the app can load a program's data (known as a 'program binary') from disk using "glProgramBinaryOES", and can supply it directly to ANGLE without having to recompile the shaders/programs. This can be significantly quicker than compiling the shaders from ESSL source code.

NOTE: To enable OES_get_program_binary, you must define GL_GLEXT_PROTOTYPES before including your OpenGL ES headers:

#define GL_GLEXT_PROTOTYPES 1
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

GetProgramBinaryHelper

We have written some helpers to make it easy to save and load program binaries in Windows Store applications. The code for this can be found below.

GetProgramBinaryHelper Sample Usage:
// Beware of your saved program binaries being invalidated, e.g. if you update your app to use new shaders
// In this case, you should set cachedShadersInvalidated = true, recompile your shaders/programs and save the new binaries
bool cachedShadersInvalidated = false;

GLuint program = glCreateProgram();

// Attempt to load the program binary from file, if the saved program binaries aren't invalid
bool loadedProgramFromFile = cachedShadersInvalidated ? false : LoadProgramHelper(program, L"myprogram.program");

if (!loadedProgramFromFile)
{
    // If you weren't able to load the program from file, then you must compile your shaders normally.
    // glShaderSource, glCompileShader, glAttachShader, glLinkProgram, etc.

    // Save the program binary to file, so that LoadProgramHelper() will succeed next time.
    // Choose any file name and extension. It should be unique to this particular program.
    SaveProgramHelper(program, L"myprogram.program");
}

// Continue to use the shader as normal.
GLint attribLoc = glGetAttribLocation(program, "aAttribute1");
glUseProgram(program);
GetProgramBinaryHelper.h:
#include "pch.h"

bool SaveProgramHelper(GLuint program, const wchar_t* fileName);

bool LoadProgramHelper(GLuint program, const wchar_t* fileName);
GetProgramBinaryHelper.cpp:
#include "pch.h"

#include <vector>

// A struct to store metadata about program binaries.
struct ShaderFileHeader
{
    GLenum binaryFormat;
    GLint programLength;
};

bool SaveProgramHelper(GLuint program, const wchar_t* fileName)
{
    ShaderFileHeader header;
    GLint writtenLength = 0;

    // First we find out the length of the program binary.
    glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH_OES, &(header.programLength));

    // Then we create a buffer of the correct length.
    std::vector<uint8_t> binaryData(header.programLength);

    // Then we retrieve the program binary.
    glGetProgramBinaryOES(program, header.programLength, &writtenLength, &(header.binaryFormat), binaryData.data());

    // Now we create a file for the buffer.
    // Note: Windows Store applications should put this in their local folder.
    auto localFolder = Windows::Storage::ApplicationData::Current->LocalFolder;
    std::wstring fullFilePath = std::wstring(localFolder->Path->Data()) + L'\\' + fileName;
    HANDLE file = CreateFile2(fullFilePath.c_str(), GENERIC_WRITE, 0, CREATE_ALWAYS, NULL);

    bool returnValue = false;

    if (file != INVALID_HANDLE_VALUE)
    {
        // Firstly write metadata about the program binary to the file.
        DWORD bytesWritten = 0;
        BOOL success = WriteFile(file, &header, sizeof(header), &bytesWritten, NULL);

        if (success && (sizeof(header) == bytesWritten))
        {
            // If successful, then we write the binary data to the file too.
            success = WriteFile(file, binaryData.data(), static_cast<DWORD>(binaryData.size()), &bytesWritten, NULL);
            if (success && (bytesWritten == binaryData.size()))
            {
                returnValue = true;
            }
        }

        CloseHandle(file);
    }

    return returnValue;
}

bool LoadProgramHelper(GLuint program, const wchar_t* fileName)
{
    // Load the requested file
    auto localFolder = Windows::Storage::ApplicationData::Current->LocalFolder;
    std::wstring fullFilePath = std::wstring(localFolder->Path->Data()) + L'\\' + fileName;
    HANDLE file = CreateFile2(fullFilePath.c_str(), GENERIC_READ, 0, OPEN_EXISTING, NULL);

    bool returnValue = false;

    if (file != INVALID_HANDLE_VALUE)
    {
        DWORD bytesRead = 0;
        ShaderFileHeader header;
        std::vector<uint8_t> binaryData(0);

        // Firstly read the metadata about the program binary.
        BOOL success = ReadFile(file, &header, sizeof(header), &bytesRead, NULL);
        if (success && (sizeof(header) == bytesRead))
        {
            FILE_STANDARD_INFO info;
            if (GetFileInformationByHandleEx(file, FileStandardInfo, &info, sizeof(info)))
            {
                // If successful, read the binary data.
                binaryData.resize(static_cast<size_t>(info.EndOfFile.QuadPart - sizeof(header)));
                success = ReadFile(file, &(binaryData[0]), static_cast<DWORD>(binaryData.size()), &bytesRead, NULL);
                if (success && (header.programLength == bytesRead) && (binaryData.size() == bytesRead))
                {
                    // If successful, update the program's data. 
                    glProgramBinaryOES(program, header.binaryFormat, &binaryData[0], static_cast<GLint>(binaryData.size()));

                    // Check the link status, which indicates whether glProgramBinaryOES() succeeded.
                    GLint linkStatus;
                    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
                    if (linkStatus != 0)
                    {
                        returnValue = true;
                    }
                    else
                    {
#ifdef _DEBUG
                        // Code to help debug programs failing to load.
                        GLint infoLogLength;
                        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);

                        if (infoLogLength > 0)
                        {
                            std::vector<GLchar> infoLog(infoLogLength);
                            glGetProgramInfoLog(program, infoLog.size(), NULL, &infoLog[0]);
                            OutputDebugStringA(&infoLog[0]);
                        }
#endif // _DEBUG
                    }
                }
            }
        }

        CloseHandle(file);
    }

    return returnValue;
}