Ensure glProgramBinary initializes uniform and attrib info

Adds Scoped* helper classes to hopefully make writing tests easier.

Bug: b/330392312
Test: GfxstreamEnd2EndTests
Change-Id: Ibb2c957682dd7173bd6f839bc1c379d47c4e0318
diff --git a/common/end2end/GfxstreamEnd2EndGlTests.cpp b/common/end2end/GfxstreamEnd2EndGlTests.cpp
index 6b6934f..ac3934d 100644
--- a/common/end2end/GfxstreamEnd2EndGlTests.cpp
+++ b/common/end2end/GfxstreamEnd2EndGlTests.cpp
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <log/log.h>
+
 #include "GfxstreamEnd2EndTests.h"
+#include "drm_fourcc.h"
 
 namespace gfxstream {
 namespace tests {
@@ -26,39 +29,106 @@
 using testing::Le;
 using testing::Not;
 
-class GfxstreamEnd2EndGlTest : public GfxstreamEnd2EndTest {};
+struct PixelR8G8B8A8 {
+    PixelR8G8B8A8(uint8_t rr, uint8_t gg, uint8_t bb, uint8_t aa) : r(rr), g(gg), b(bb), a(aa) {}
+
+    PixelR8G8B8A8(int xx, int yy, uint8_t rr, uint8_t gg, uint8_t bb, uint8_t aa)
+        : x(xx), y(yy), r(rr), g(gg), b(bb), a(aa) {}
+
+    std::optional<int> x;
+    std::optional<int> y;
+
+    uint8_t r;
+    uint8_t g;
+    uint8_t b;
+    uint8_t a;
+
+    std::string ToString() const {
+        std::string ret = std::string("Pixel");
+        if (x) {
+            ret += std::string(" x:") + std::to_string(*x);
+        }
+        if (y) {
+            ret += std::string(" y:") + std::to_string(*y);
+        }
+        ret += std::string(" {");
+        ret += std::string(" r:") + std::to_string(static_cast<int>(r));
+        ret += std::string(" g:") + std::to_string(static_cast<int>(g));
+        ret += std::string(" b:") + std::to_string(static_cast<int>(b));
+        ret += std::string(" a:") + std::to_string(static_cast<int>(a));
+        ret += std::string(" }");
+        return ret;
+    }
+
+    friend void PrintTo(const PixelR8G8B8A8& pixel, std::ostream* os) { *os << pixel.ToString(); }
+};
+
+MATCHER_P4(IsOkWithRGBA, r, g, b, a,
+           std::string(" equals ") + PixelR8G8B8A8(r, g, b, a).ToString()) {
+    const auto& actual = arg;
+
+    if (actual.ok() && actual.value().r == r && actual.value().g == g && actual.value().b == b &&
+        actual.value().a == a) {
+        return true;
+    }
+
+    if (actual.ok()) {
+        *result_listener << "actual: " << actual.value().ToString();
+    } else {
+        *result_listener << "actual: {" << " error: " << actual.error() << " };";
+    }
+    return false;
+}
+
+class GfxstreamEnd2EndGlTest : public GfxstreamEnd2EndTest {
+   protected:
+    GlExpected<PixelR8G8B8A8> GetPixelAt(GLint x, GLint y) {
+        if (!mGl) {
+            return android::base::unexpected("GL not available, running with `with_gl = false`?");
+        }
+
+        GLubyte rgba[4] = {0, 0, 0, 0};
+        mGl->glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
+
+        if (GLenum error = mGl->glGetError(); error != GL_NO_ERROR) {
+            return android::base::unexpected("Failed to glReadPixels() with error " +
+                                             std::to_string(error));
+        }
+
+        return PixelR8G8B8A8(x, y, rgba[0], rgba[1], rgba[2], rgba[3]);
+    }
+
+    void SetUp() override {
+        GfxstreamEnd2EndTest::SetUp();
+
+        SetUpEglContextAndSurface(2, mSurfaceWidth, mSurfaceHeight, &mDisplay, &mContext,
+                                  &mSurface);
+    }
+
+    void TearDown() override {
+        TearDownEglContextAndSurface(mDisplay, mContext, mSurface);
+
+        GfxstreamEnd2EndTest::TearDown();
+    }
+
+    int mSurfaceWidth = 32;
+    int mSurfaceHeight = 32;
+    EGLDisplay mDisplay;
+    EGLContext mContext;
+    EGLSurface mSurface;
+};
 
 TEST_P(GfxstreamEnd2EndGlTest, BasicViewport) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(2, width, height, &display, &context, &surface);
-
     GLint viewport[4] = {};
     mGl->glGetIntegerv(GL_VIEWPORT, viewport);
 
     EXPECT_THAT(viewport[0], Eq(0));
     EXPECT_THAT(viewport[1], Eq(0));
-    EXPECT_THAT(viewport[2], Eq(width));
-    EXPECT_THAT(viewport[3], Eq(height));
-
-    TearDownEglContextAndSurface(display, context, surface);
+    EXPECT_THAT(viewport[2], Eq(mSurfaceWidth));
+    EXPECT_THAT(viewport[3], Eq(mSurfaceHeight));
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, CreateWindowSurface) {
-
-    EGLDisplay display = mGl->eglGetDisplay(EGL_DEFAULT_DISPLAY);
-    ASSERT_THAT(display, Not(Eq(EGL_NO_DISPLAY)));
-
-    int versionMajor = 0;
-    int versionMinor = 0;
-    ASSERT_THAT(mGl->eglInitialize(display, &versionMajor, &versionMinor), IsTrue());
-
-    ASSERT_THAT(mGl->eglBindAPI(EGL_OPENGL_ES_API), IsTrue());
-
     // clang-format off
     static const EGLint configAttributes[] = {
         EGL_SURFACE_TYPE,    EGL_PBUFFER_BIT,
@@ -68,11 +138,13 @@
     // clang-format on
 
     int numConfigs = 0;
-    ASSERT_THAT(mGl->eglChooseConfig(display, configAttributes, nullptr, 1, &numConfigs), IsTrue());
+    ASSERT_THAT(mGl->eglChooseConfig(mDisplay, configAttributes, nullptr, 1, &numConfigs),
+                IsTrue());
     ASSERT_THAT(numConfigs, Gt(0));
 
     EGLConfig config = nullptr;
-    ASSERT_THAT(mGl->eglChooseConfig(display, configAttributes, &config, 1, &numConfigs), IsTrue());
+    ASSERT_THAT(mGl->eglChooseConfig(mDisplay, configAttributes, &config, 1, &numConfigs),
+                IsTrue());
     ASSERT_THAT(config, Not(Eq(nullptr)));
 
     // clang-format off
@@ -82,7 +154,7 @@
     };
     // clang-format on
 
-    EGLContext context = mGl->eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
+    EGLContext context = mGl->eglCreateContext(mDisplay, config, EGL_NO_CONTEXT, contextAttribs);
     ASSERT_THAT(context, Not(Eq(EGL_NO_CONTEXT)));
 
     constexpr const int width = 32;
@@ -90,10 +162,10 @@
 
     auto anw = mAnwHelper->createNativeWindowForTesting(mGralloc.get(), width, height);
 
-    EGLSurface surface = mGl->eglCreateWindowSurface(display, config, anw, nullptr);
+    EGLSurface surface = mGl->eglCreateWindowSurface(mDisplay, config, anw, nullptr);
     ASSERT_THAT(surface, Not(Eq(EGL_NO_SURFACE)));
 
-    ASSERT_THAT(mGl->eglMakeCurrent(display, surface, surface, context), IsTrue());
+    ASSERT_THAT(mGl->eglMakeCurrent(mDisplay, surface, surface, context), IsTrue());
 
     constexpr const int iterations = 120;
     for (int i = 0; i < iterations; i++) {
@@ -101,48 +173,29 @@
         mGl->glClearColor(1.0f, 0.0f, static_cast<float>(i) / static_cast<float>(iterations), 1.0f);
         mGl->glClear(GL_COLOR_BUFFER_BIT);
         mGl->glFinish();
-        mGl->eglSwapBuffers(display, surface);
+        mGl->eglSwapBuffers(mDisplay, surface);
     }
 
-    ASSERT_THAT(mGl->eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT), IsTrue());
-    ASSERT_THAT(mGl->eglDestroyContext(display, context), IsTrue());
-    ASSERT_THAT(mGl->eglDestroySurface(display, surface), IsTrue());
-    mAnwHelper->release(anw);
+    ASSERT_THAT(mGl->eglDestroyContext(mDisplay, context), IsTrue());
+    ASSERT_THAT(mGl->eglDestroySurface(mDisplay, surface), IsTrue());
 
-    TearDownGuest();
+    mAnwHelper->release(anw);
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, SwitchContext) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(2, width, height, &display, &context, &surface);
-
-    ASSERT_THAT(mGl->eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT), IsTrue());
+    ASSERT_THAT(mGl->eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT),
+                IsTrue());
     for (int i = 0; i < 100; i++) {
-        ASSERT_THAT(mGl->eglMakeCurrent(display, surface, surface, context), IsTrue());
-        ASSERT_THAT(mGl->eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT), IsTrue());
+        ASSERT_THAT(mGl->eglMakeCurrent(mDisplay, mSurface, mSurface, mContext), IsTrue());
+        ASSERT_THAT(mGl->eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT),
+                    IsTrue());
     }
-
-    TearDownEglContextAndSurface(display, context, surface);
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, MappedMemory) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(3, width, height, &display, &context, &surface);
-
     constexpr GLsizei kBufferSize = 64;
 
-    GLuint buffer;
-    mGl->glGenBuffers(1, &buffer);
+    ScopedGlBuffer buffer(*mGl);
     mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
     mGl->glBufferData(GL_ARRAY_BUFFER, kBufferSize, 0, GL_DYNAMIC_DRAW);
 
@@ -176,9 +229,6 @@
     }
 
     mGl->glBindBuffer(GL_ARRAY_BUFFER, 0);
-    mGl->glDeleteBuffers(1, &buffer);
-
-    TearDownEglContextAndSurface(display, context, surface);
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, ContextStrings) {
@@ -270,14 +320,6 @@
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, FramebufferFetchShader) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(3, width, height, &display, &context, &surface);
-
     const std::string extensionsString = (const char*)mGl->glGetString(GL_EXTENSIONS);
     ASSERT_THAT(extensionsString, Not(IsEmpty()));
 
@@ -297,23 +339,12 @@
     auto result = SetUpShader(GL_FRAGMENT_SHADER, shaderSource);
     if (result.ok()) {
         ASSERT_THAT(supportsFramebufferFetch, Eq(GL_TRUE));
-        mGl->glDeleteShader(*result);
     } else {
         ASSERT_THAT(supportsFramebufferFetch, Eq(GL_FALSE));
     }
-
-    TearDownEglContextAndSurface(display, context, surface);
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, ConstantMatrixShader) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(2, width, height, &display, &context, &surface);
-
     const std::string shaderSource = R"(\
 #version 300 es
 precision mediump float;
@@ -334,20 +365,9 @@
 
     auto result = SetUpShader(GL_VERTEX_SHADER, shaderSource);
     ASSERT_THAT(result, IsOk());
-    mGl->glDeleteShader(result.value());
-
-    TearDownEglContextAndSurface(display, context, surface);
 }
 
 TEST_P(GfxstreamEnd2EndGlTest, Draw) {
-    constexpr const int width = 32;
-    constexpr const int height = 32;
-
-    EGLDisplay display;
-    EGLContext context;
-    EGLSurface surface;
-    SetUpEglContextAndSurface(2, width, height, &display, &context, &surface);
-
     const std::string vertSource = R"(\
 #version 300 es
 precision highp float;
@@ -378,9 +398,7 @@
 }
     )";
 
-    auto programResult = SetUpProgram(vertSource, fragSource);
-    ASSERT_THAT(programResult, IsOk());
-    auto program = programResult.value();
+    ScopedGlProgram program = GL_ASSERT(SetUpProgram(vertSource, fragSource));
 
     GLint transformUniformLocation = mGl->glGetUniformLocation(program, "transform");
     mGl->glEnableVertexAttribArray(0);
@@ -398,8 +416,7 @@
         // clang-format on
     };
 
-    GLuint buffer;
-    mGl->glGenBuffers(1, &buffer);
+    ScopedGlBuffer buffer(*mGl);
     mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
     mGl->glBufferData(GL_ARRAY_BUFFER, sizeof(vertexAttrs), vertexAttrs, GL_STATIC_DRAW);
 
@@ -430,14 +447,303 @@
     }
 
     mGl->glFinish();
-
     mGl->glBindBuffer(GL_ARRAY_BUFFER, 0);
-    mGl->glDeleteBuffers(1, &buffer);
-
     mGl->glUseProgram(0);
-    mGl->glDeleteProgram(program);
+}
 
-    TearDownEglContextAndSurface(display, context, surface);
+TEST_P(GfxstreamEnd2EndGlTest, ProgramBinaryWithAHB) {
+    const uint32_t width = 2;
+    const uint32_t height = 2;
+    auto ahb =
+        GL_ASSERT(ScopedAHardwareBuffer::Allocate(*mGralloc, width, height, DRM_FORMAT_ABGR8888));
+
+    {
+        uint8_t* mapped = GL_ASSERT(ahb.Lock());
+        uint32_t pos = 0;
+        for (uint32_t h = 0; h < height; h++) {
+            for (uint32_t w = 0; w < width; w++) {
+                mapped[pos++] = 0;
+                mapped[pos++] = 0;
+                mapped[pos++] = 128;
+                mapped[pos++] = 255;
+            }
+        }
+        ahb.Unlock();
+    }
+
+    const EGLint ahbImageAttribs[] = {
+        // clang-format off
+        EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
+        EGL_NONE,
+        // clang-format on
+    };
+    EGLImageKHR ahbImage = mGl->eglCreateImageKHR(mDisplay, EGL_NO_CONTEXT,
+                                                  EGL_NATIVE_BUFFER_ANDROID, ahb, ahbImageAttribs);
+    ASSERT_THAT(ahbImage, Not(Eq(EGL_NO_IMAGE_KHR)));
+
+    ScopedGlTexture ahbTexture(*mGl);
+    mGl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, ahbTexture);
+    mGl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    mGl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    mGl->glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, ahbImage);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    GLenum programBinaryFormat = GL_NONE;
+    std::vector<uint8_t> programBinaryData;
+    {
+        const std::string vertSource = R"(\
+            #version 300 es
+
+            layout (location = 0) in vec2 pos;
+            layout (location = 1) in vec2 tex;
+
+            out vec2 vTex;
+
+            void main() {
+                gl_Position = vec4(pos, 0.0, 1.0);
+                vTex = tex;
+            })";
+
+        const std::string fragSource = R"(\
+            #version 300 es
+
+            precision highp float;
+
+            uniform float uMultiplier;
+            uniform sampler2D uTexture;
+
+            in vec2 vTex;
+
+            out vec4 oColor;
+
+            void main() {
+                oColor = texture(uTexture, vTex) * uMultiplier;
+            })";
+
+        ScopedGlProgram program = GL_ASSERT(SetUpProgram(vertSource, fragSource));
+
+        GLint programBinaryLength = 0;
+        mGl->glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &programBinaryLength);
+        ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+        programBinaryData.resize(programBinaryLength);
+
+        GLint readProgramBinaryLength = 0;
+        mGl->glGetProgramBinary(program, programBinaryLength, &readProgramBinaryLength,
+                                &programBinaryFormat, programBinaryData.data());
+        ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+        ASSERT_THAT(programBinaryLength, Eq(readProgramBinaryLength));
+    }
+
+    ScopedGlProgram program = GL_ASSERT(SetUpProgram(programBinaryFormat, programBinaryData));
+    ASSERT_THAT(program, Not(Eq(0)));
+
+    GLint textureUniformLoc = mGl->glGetUniformLocation(program, "uTexture");
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(textureUniformLoc, Not(Eq(-1)));
+
+    GLint multiplierUniformLoc = mGl->glGetUniformLocation(program, "uMultiplier");
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(multiplierUniformLoc, Not(Eq(-1)));
+
+    const GLsizei kFramebufferWidth = 4;
+    const GLsizei kFramebufferHeight = 4;
+    ScopedGlFramebuffer framebuffer(*mGl);
+    mGl->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    ScopedGlTexture framebufferTexture(*mGl);
+    mGl->glBindTexture(GL_TEXTURE_2D, framebufferTexture);
+    mGl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kFramebufferWidth, kFramebufferHeight, 0, GL_RGBA,
+                      GL_UNSIGNED_BYTE, nullptr);
+    mGl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                framebufferTexture, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(mGl->glCheckFramebufferStatus(GL_FRAMEBUFFER), Eq(GL_FRAMEBUFFER_COMPLETE));
+    mGl->glBindTexture(GL_TEXTURE_2D, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    struct VertexAttributes {
+        float pos[2];
+        float tex[2];
+    };
+    const VertexAttributes vertexAttrs[] = {
+        // clang-format off
+        { { -1.0f, -1.0f,}, { 0.0f, 0.0f }, },
+        { {  3.0f, -1.0f,}, { 2.0f, 0.0f }, },
+        { { -1.0f,  3.0f,}, { 0.0f, 2.0f }, },
+        // clang-format on
+    };
+    ScopedGlBuffer buffer(*mGl);
+    mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    mGl->glBufferData(GL_ARRAY_BUFFER, sizeof(vertexAttrs), vertexAttrs, GL_STATIC_DRAW);
+
+    mGl->glUseProgram(program);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    mGl->glEnableVertexAttribArray(0);
+    mGl->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(VertexAttributes),
+                               (GLvoid*)offsetof(VertexAttributes, pos));
+    mGl->glEnableVertexAttribArray(1);
+    mGl->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(VertexAttributes),
+                               (GLvoid*)offsetof(VertexAttributes, tex));
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glActiveTexture(GL_TEXTURE0);
+    mGl->glBindTexture(GL_TEXTURE_2D, ahbTexture);
+    mGl->glUniform1i(textureUniformLoc, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glUniform1f(multiplierUniformLoc, 2.0f);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glDrawArrays(GL_TRIANGLES, 0, 3);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    for (int x = 0; x < kFramebufferWidth; x++) {
+        for (int y = 0; y < kFramebufferHeight; y++) {
+            EXPECT_THAT(GetPixelAt(x, y), IsOkWithRGBA(0, 0, 255, 255));
+        }
+    }
+
+    mGl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+TEST_P(GfxstreamEnd2EndGlTest, ProgramBinaryWithTexture) {
+    const GLsizei kTextureWidth = 2;
+    const GLsizei kTextureHeight = 2;
+    const GLubyte kTextureData[16] = {
+        // clang-format off
+        0, 0, 128, 255,   0, 0, 128, 255,
+
+        0, 0, 128, 255,   0, 0, 128, 255,
+        // clang-format on
+    };
+    ScopedGlTexture texture(*mGl);
+    mGl->glBindTexture(GL_TEXTURE_2D, texture);
+    mGl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    mGl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    mGl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTextureWidth, kTextureHeight, 0, GL_RGBA,
+                      GL_UNSIGNED_BYTE, kTextureData);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    GLenum programBinaryFormat = GL_NONE;
+    std::vector<uint8_t> programBinaryData;
+    {
+        const std::string vertSource = R"(\
+            #version 300 es
+
+            layout (location = 0) in vec2 pos;
+            layout (location = 1) in vec2 tex;
+
+            out vec2 vTex;
+
+            void main() {
+                gl_Position = vec4(pos, 0.0, 1.0);
+                vTex = tex;
+            })";
+
+        const std::string fragSource = R"(\
+            #version 300 es
+
+            precision highp float;
+
+            uniform float uMultiplier;
+            uniform sampler2D uTexture;
+
+            in vec2 vTex;
+
+            out vec4 oColor;
+
+            void main() {
+                oColor = texture(uTexture, vTex) * uMultiplier;
+            })";
+
+        ScopedGlProgram program = GL_ASSERT(SetUpProgram(vertSource, fragSource));
+
+        GLint programBinaryLength = 0;
+        mGl->glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &programBinaryLength);
+        ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+        programBinaryData.resize(programBinaryLength);
+
+        GLint readProgramBinaryLength = 0;
+        mGl->glGetProgramBinary(program, programBinaryLength, &readProgramBinaryLength,
+                                &programBinaryFormat, programBinaryData.data());
+        ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+        ASSERT_THAT(programBinaryLength, Eq(readProgramBinaryLength));
+    }
+
+    ScopedGlProgram program = GL_ASSERT(SetUpProgram(programBinaryFormat, programBinaryData));
+    ASSERT_THAT(program, Not(Eq(0)));
+
+    GLint textureUniformLoc = mGl->glGetUniformLocation(program, "uTexture");
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(textureUniformLoc, Not(Eq(-1)));
+
+    GLint multiplierUniformLoc = mGl->glGetUniformLocation(program, "uMultiplier");
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(multiplierUniformLoc, Not(Eq(-1)));
+
+    const GLsizei kFramebufferWidth = 4;
+    const GLsizei kFramebufferHeight = 4;
+    ScopedGlFramebuffer framebuffer(*mGl);
+    mGl->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    ScopedGlTexture framebufferTexture(*mGl);
+    mGl->glBindTexture(GL_TEXTURE_2D, framebufferTexture);
+    mGl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kFramebufferWidth, kFramebufferHeight, 0, GL_RGBA,
+                      GL_UNSIGNED_BYTE, nullptr);
+    mGl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                                framebufferTexture, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+    ASSERT_THAT(mGl->glCheckFramebufferStatus(GL_FRAMEBUFFER), Eq(GL_FRAMEBUFFER_COMPLETE));
+    mGl->glBindTexture(GL_TEXTURE_2D, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    struct VertexAttributes {
+        float pos[2];
+        float tex[2];
+    };
+    const VertexAttributes vertexAttrs[] = {
+        // clang-format off
+        { { -1.0f, -1.0f,}, { 0.0f, 0.0f }, },
+        { {  3.0f, -1.0f,}, { 2.0f, 0.0f }, },
+        { { -1.0f,  3.0f,}, { 0.0f, 2.0f }, },
+        // clang-format on
+    };
+    ScopedGlBuffer buffer(*mGl);
+    mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    mGl->glBufferData(GL_ARRAY_BUFFER, sizeof(vertexAttrs), vertexAttrs, GL_STATIC_DRAW);
+
+    mGl->glUseProgram(program);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    mGl->glEnableVertexAttribArray(0);
+    mGl->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(VertexAttributes),
+                               (GLvoid*)offsetof(VertexAttributes, pos));
+    mGl->glEnableVertexAttribArray(1);
+    mGl->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(VertexAttributes),
+                               (GLvoid*)offsetof(VertexAttributes, tex));
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glActiveTexture(GL_TEXTURE0);
+    mGl->glBindTexture(GL_TEXTURE_2D, texture);
+    mGl->glUniform1i(textureUniformLoc, 0);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glUniform1f(multiplierUniformLoc, 2.0f);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    mGl->glDrawArrays(GL_TRIANGLES, 0, 3);
+    ASSERT_THAT(mGl->glGetError(), Eq(GL_NO_ERROR));
+
+    for (int x = 0; x < kFramebufferWidth; x++) {
+        for (int y = 0; y < kFramebufferHeight; y++) {
+            EXPECT_THAT(GetPixelAt(x, y), IsOkWithRGBA(0, 0, 255, 255));
+        }
+    }
+
+    mGl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
 }
 
 INSTANTIATE_TEST_CASE_P(GfxstreamEnd2EndTests, GfxstreamEnd2EndGlTest,
diff --git a/common/end2end/GfxstreamEnd2EndTests.cpp b/common/end2end/GfxstreamEnd2EndTests.cpp
index 5d698b9..16dee00 100644
--- a/common/end2end/GfxstreamEnd2EndTests.cpp
+++ b/common/end2end/GfxstreamEnd2EndTests.cpp
@@ -83,7 +83,7 @@
     return info.param.ToString();
 }
 
-std::unique_ptr<GfxstreamEnd2EndTest::GuestGlDispatchTable> GfxstreamEnd2EndTest::SetupGuestGl() {
+std::unique_ptr<GuestGlDispatchTable> GfxstreamEnd2EndTest::SetupGuestGl() {
     const std::filesystem::path testDirectory = gfxstream::guest::getProgramDirectory();
     const std::string eglLibPath = (testDirectory / "libEGL_emulation_with_host.so").string();
     const std::string gles2LibPath = (testDirectory / "libGLESv2_emulation_with_host.so").string();
@@ -117,11 +117,13 @@
     LIST_RENDER_EGL_FUNCTIONS(LOAD_EGL_FUNCTION)
     LIST_RENDER_EGL_EXTENSIONS_FUNCTIONS(LOAD_EGL_FUNCTION)
 
-    #define LOAD_GLES2_FUNCTION(return_type, function_name, signature, callargs)    \
-        gl-> function_name = reinterpret_cast< return_type (*) signature >(eglGetAddr( #function_name )); \
-        if (!gl-> function_name) { \
-            gl-> function_name = reinterpret_cast< return_type (*) signature >(dlsym(gles2Lib, #function_name)); \
-        }
+#define LOAD_GLES2_FUNCTION(return_type, function_name, signature, callargs)         \
+    gl->function_name =                                                              \
+        reinterpret_cast<return_type(*) signature>(dlsym(gles2Lib, #function_name)); \
+    if (!gl->function_name) {                                                        \
+        gl->function_name =                                                          \
+            reinterpret_cast<return_type(*) signature>(eglGetAddr(#function_name));  \
+    }
 
     LIST_GLES_FUNCTIONS(LOAD_GLES2_FUNCTION, LOAD_GLES2_FUNCTION)
 
@@ -290,91 +292,144 @@
     ASSERT_THAT(mGl->eglDestroySurface(display, surface), IsTrue());
 }
 
-GlExpected<GLuint> GfxstreamEnd2EndTest::SetUpShader(GLenum type, const std::string& source) {
-    if (!mGl) {
-        return android::base::unexpected("Gl not enabled for this test.");
-    }
-
-    GLuint shader = mGl->glCreateShader(type);
+GlExpected<ScopedGlShader> ScopedGlShader::MakeShader(GlDispatch& dispatch, GLenum type,
+                                                      const std::string& source) {
+    GLuint shader = dispatch.glCreateShader(type);
     if (!shader) {
         return android::base::unexpected("Failed to create shader.");
     }
 
     const GLchar* sourceTyped = (const GLchar*)source.c_str();
     const GLint sourceLength = source.size();
-    mGl->glShaderSource(shader, 1, &sourceTyped, &sourceLength);
-    mGl->glCompileShader(shader);
-
-    GLenum err = mGl->glGetError();
-    if (err != GL_NO_ERROR) {
-        return android::base::unexpected("Failed to compile shader.");
-    }
+    dispatch.glShaderSource(shader, 1, &sourceTyped, &sourceLength);
+    dispatch.glCompileShader(shader);
 
     GLint compileStatus;
-    mGl->glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
+    dispatch.glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
 
-    if (compileStatus == GL_TRUE) {
-        return shader;
-    } else {
+    if (compileStatus != GL_TRUE) {
         GLint errorLogLength = 0;
-        mGl->glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &errorLogLength);
+        dispatch.glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &errorLogLength);
         if (!errorLogLength) {
             errorLogLength = 512;
         }
 
         std::vector<GLchar> errorLog(errorLogLength);
-        mGl->glGetShaderInfoLog(shader, errorLogLength, &errorLogLength, errorLog.data());
+        dispatch.glGetShaderInfoLog(shader, errorLogLength, &errorLogLength, errorLog.data());
 
         const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
         ALOGE("Shader compilation failed with: \"%s\"", errorString.c_str());
 
-        mGl->glDeleteShader(shader);
+        dispatch.glDeleteShader(shader);
         return android::base::unexpected(errorString);
     }
+
+    return ScopedGlShader(dispatch, shader);
 }
 
-GlExpected<GLuint> GfxstreamEnd2EndTest::SetUpProgram(
-        const std::string& vertSource,
-        const std::string& fragSource) {
-    auto vertResult = SetUpShader(GL_VERTEX_SHADER, vertSource);
-    if (!vertResult.ok()) {
-        return vertResult;
-    }
-    auto vertShader = vertResult.value();
+GlExpected<ScopedGlProgram> ScopedGlProgram::MakeProgram(GlDispatch& dispatch,
+                                                         const std::string& vertSource,
+                                                         const std::string& fragSource) {
+    auto vertShader = GL_EXPECT(ScopedGlShader::MakeShader(dispatch, GL_VERTEX_SHADER, vertSource));
+    auto fragShader =
+        GL_EXPECT(ScopedGlShader::MakeShader(dispatch, GL_FRAGMENT_SHADER, fragSource));
 
-    auto fragResult = SetUpShader(GL_FRAGMENT_SHADER, fragSource);
-    if (!fragResult.ok()) {
-        return fragResult;
-    }
-    auto fragShader = fragResult.value();
-
-    GLuint program = mGl->glCreateProgram();
-    mGl->glAttachShader(program, vertShader);
-    mGl->glAttachShader(program, fragShader);
-    mGl->glLinkProgram(program);
-    mGl->glDeleteShader(vertShader);
-    mGl->glDeleteShader(fragShader);
+    GLuint program = dispatch.glCreateProgram();
+    dispatch.glAttachShader(program, vertShader);
+    dispatch.glAttachShader(program, fragShader);
+    dispatch.glLinkProgram(program);
 
     GLint linkStatus;
-    mGl->glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
-    if (linkStatus == GL_TRUE) {
-        return program;
-    } else {
+    dispatch.glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    if (linkStatus != GL_TRUE) {
         GLint errorLogLength = 0;
-        mGl->glGetProgramiv(program, GL_INFO_LOG_LENGTH, &errorLogLength);
+        dispatch.glGetProgramiv(program, GL_INFO_LOG_LENGTH, &errorLogLength);
         if (!errorLogLength) {
             errorLogLength = 512;
         }
 
         std::vector<char> errorLog(errorLogLength, 0);
-        mGl->glGetProgramInfoLog(program, errorLogLength, nullptr, errorLog.data());
+        dispatch.glGetProgramInfoLog(program, errorLogLength, nullptr, errorLog.data());
 
         const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
         ALOGE("Program link failed with: \"%s\"", errorString.c_str());
 
-        mGl->glDeleteProgram(program);
+        dispatch.glDeleteProgram(program);
         return android::base::unexpected(errorString);
     }
+
+    return ScopedGlProgram(dispatch, program);
+}
+
+GlExpected<ScopedGlProgram> ScopedGlProgram::MakeProgram(
+    GlDispatch& dispatch, GLenum programBinaryFormat,
+    const std::vector<uint8_t>& programBinaryData) {
+    GLuint program = dispatch.glCreateProgram();
+    dispatch.glProgramBinary(program, programBinaryFormat, programBinaryData.data(),
+                             programBinaryData.size());
+
+    GLint linkStatus;
+    dispatch.glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    if (linkStatus != GL_TRUE) {
+        GLint errorLogLength = 0;
+        dispatch.glGetProgramiv(program, GL_INFO_LOG_LENGTH, &errorLogLength);
+        if (!errorLogLength) {
+            errorLogLength = 512;
+        }
+
+        std::vector<char> errorLog(errorLogLength, 0);
+        dispatch.glGetProgramInfoLog(program, errorLogLength, nullptr, errorLog.data());
+
+        const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
+        ALOGE("Program link failed with: \"%s\"", errorString.c_str());
+
+        dispatch.glDeleteProgram(program);
+        return android::base::unexpected(errorString);
+    }
+
+    return ScopedGlProgram(dispatch, program);
+}
+
+GlExpected<ScopedAHardwareBuffer> ScopedAHardwareBuffer::Allocate(Gralloc& gralloc, uint32_t width,
+                                                                  uint32_t height,
+                                                                  uint32_t format) {
+    AHardwareBuffer* ahb = nullptr;
+    int status = gralloc.allocate(width, height, format, -1, &ahb);
+    if (status != 0) {
+        return android::base::unexpected(std::string("Failed to allocate AHB with width:") +
+                                         std::to_string(width) + std::string(" height:") +
+                                         std::to_string(height) + std::string(" format:") +
+                                         std::to_string(format));
+    }
+
+    return ScopedAHardwareBuffer(gralloc, ahb);
+}
+
+GlExpected<ScopedGlShader> GfxstreamEnd2EndTest::SetUpShader(GLenum type,
+                                                             const std::string& source) {
+    if (!mGl) {
+        return android::base::unexpected("Gl not enabled for this test.");
+    }
+
+    return ScopedGlShader::MakeShader(*mGl, type, source);
+}
+
+GlExpected<ScopedGlProgram> GfxstreamEnd2EndTest::SetUpProgram(const std::string& vertSource,
+                                                               const std::string& fragSource) {
+    if (!mGl) {
+        return android::base::unexpected("Gl not enabled for this test.");
+    }
+
+    return ScopedGlProgram::MakeProgram(*mGl, vertSource, fragSource);
+}
+
+GlExpected<ScopedGlProgram> GfxstreamEnd2EndTest::SetUpProgram(
+    GLenum programBinaryFormat, const std::vector<uint8_t>& programBinaryData) {
+    if (!mGl) {
+        return android::base::unexpected("Gl not enabled for this test.");
+    }
+
+    return ScopedGlProgram::MakeProgram(*mGl, programBinaryFormat, programBinaryData);
 }
 
 VkExpected<GfxstreamEnd2EndTest::TypicalVkTestEnvironment>
diff --git a/common/end2end/GfxstreamEnd2EndTests.h b/common/end2end/GfxstreamEnd2EndTests.h
index 34fd728..7b1ca12 100644
--- a/common/end2end/GfxstreamEnd2EndTests.h
+++ b/common/end2end/GfxstreamEnd2EndTests.h
@@ -89,6 +89,24 @@
 template <typename GlType>
 using GlExpected = android::base::expected<GlType, std::string>;
 
+#define GL_ASSERT(x)                        \
+    ({                                      \
+        auto gl_result = (x);               \
+        if (!gl_result.ok()) {              \
+            ASSERT_THAT(gl_result, IsOk()); \
+        }                                   \
+        std::move(gl_result.value());       \
+    })
+
+#define GL_EXPECT(x)                                             \
+    ({                                                           \
+        auto gl_result = (x);                                    \
+        if (!gl_result.ok()) {                                   \
+            return android::base::unexpected(gl_result.error()); \
+        }                                                        \
+        std::move(gl_result.value());                            \
+    })
+
 template <typename VkType>
 using VkExpected = android::base::expected<VkType, vkhpp::Result>;
 
@@ -142,6 +160,219 @@
     std::move(vkhpp_result_value.value);                                      \
   })
 
+struct GuestGlDispatchTable {
+#define DECLARE_EGL_FUNCTION(return_type, function_name, signature) \
+    return_type(*function_name) signature = nullptr;
+
+#define DECLARE_GLES_FUNCTION(return_type, function_name, signature, args) \
+    return_type(*function_name) signature = nullptr;
+
+    LIST_RENDER_EGL_FUNCTIONS(DECLARE_EGL_FUNCTION)
+    LIST_RENDER_EGL_EXTENSIONS_FUNCTIONS(DECLARE_EGL_FUNCTION)
+    LIST_GLES_FUNCTIONS(DECLARE_GLES_FUNCTION, DECLARE_GLES_FUNCTION)
+};
+
+class ScopedGlType {
+   public:
+    using GlDispatch = GuestGlDispatchTable;
+    using GlDispatchGenFunc = void (*GuestGlDispatchTable::*)(GLsizei, GLuint*);
+    using GlDispatchDelFunc = void (*GuestGlDispatchTable::*)(GLsizei, const GLuint*);
+
+    ScopedGlType() {}
+
+    ScopedGlType(GlDispatch& glDispatch, GlDispatchGenFunc glGenFunc, GlDispatchDelFunc glDelFunc)
+        : mGlDispatch(&glDispatch), mGlGenFunc(glGenFunc), mGlDelFunc(glDelFunc) {
+        (mGlDispatch->*mGlGenFunc)(1, &mHandle);
+    }
+
+    ScopedGlType(const ScopedGlType& rhs) = delete;
+    ScopedGlType& operator=(const ScopedGlType& rhs) = delete;
+
+    ScopedGlType(ScopedGlType&& rhs)
+        : mGlDispatch(rhs.mGlDispatch),
+          mGlGenFunc(rhs.mGlGenFunc),
+          mGlDelFunc(rhs.mGlDelFunc),
+          mHandle(rhs.mHandle) {
+        rhs.mHandle = 0;
+    }
+
+    ScopedGlType& operator=(ScopedGlType&& rhs) {
+        mGlDispatch = rhs.mGlDispatch;
+        mGlGenFunc = rhs.mGlGenFunc;
+        mGlDelFunc = rhs.mGlDelFunc;
+        std::swap(mHandle, rhs.mHandle);
+        return *this;
+    }
+
+    ~ScopedGlType() {
+        if (mHandle != 0) {
+            (mGlDispatch->*mGlDelFunc)(1, &mHandle);
+            mHandle = 0;
+        }
+    }
+
+    operator GLuint() { return mHandle; }
+    operator GLuint() const { return mHandle; }
+
+   private:
+    GlDispatch* mGlDispatch = nullptr;
+    GlDispatchGenFunc mGlGenFunc = nullptr;
+    GlDispatchDelFunc mGlDelFunc = nullptr;
+    GLuint mHandle = 0;
+};
+
+class ScopedGlBuffer : public ScopedGlType {
+   public:
+    ScopedGlBuffer(GlDispatch& dispatch)
+        : ScopedGlType(dispatch, &GlDispatch::glGenBuffers, &GlDispatch::glDeleteBuffers) {}
+};
+
+class ScopedGlTexture : public ScopedGlType {
+   public:
+    ScopedGlTexture(GlDispatch& dispatch)
+        : ScopedGlType(dispatch, &GlDispatch::glGenTextures, &GlDispatch::glDeleteTextures) {}
+};
+
+class ScopedGlFramebuffer : public ScopedGlType {
+   public:
+    ScopedGlFramebuffer(GlDispatch& dispatch)
+        : ScopedGlType(dispatch, &GlDispatch::glGenFramebuffers,
+                       &GlDispatch::glDeleteFramebuffers) {}
+};
+
+class ScopedGlShader {
+   public:
+    using GlDispatch = GuestGlDispatchTable;
+
+    ScopedGlShader() = default;
+
+    ScopedGlShader(const ScopedGlShader& rhs) = delete;
+    ScopedGlShader& operator=(const ScopedGlShader& rhs) = delete;
+
+    static GlExpected<ScopedGlShader> MakeShader(GlDispatch& dispatch, GLenum type,
+                                                 const std::string& source);
+
+    ScopedGlShader(ScopedGlShader&& rhs) : mGlDispatch(rhs.mGlDispatch), mHandle(rhs.mHandle) {
+        rhs.mHandle = 0;
+    }
+
+    ScopedGlShader& operator=(ScopedGlShader&& rhs) {
+        mGlDispatch = rhs.mGlDispatch;
+        std::swap(mHandle, rhs.mHandle);
+        return *this;
+    }
+
+    ~ScopedGlShader() {
+        if (mHandle != 0) {
+            mGlDispatch->glDeleteShader(mHandle);
+            mHandle = 0;
+        }
+    }
+
+    operator GLuint() { return mHandle; }
+    operator GLuint() const { return mHandle; }
+
+   private:
+    ScopedGlShader(GlDispatch& dispatch, GLuint handle) : mGlDispatch(&dispatch), mHandle(handle) {}
+
+    GlDispatch* mGlDispatch = nullptr;
+    GLuint mHandle = 0;
+};
+
+class ScopedGlProgram {
+   public:
+    using GlDispatch = GuestGlDispatchTable;
+
+    ScopedGlProgram() = default;
+
+    ScopedGlProgram(const ScopedGlProgram& rhs) = delete;
+    ScopedGlProgram& operator=(const ScopedGlProgram& rhs) = delete;
+
+    static GlExpected<ScopedGlProgram> MakeProgram(GlDispatch& dispatch,
+                                                   const std::string& vertShader,
+                                                   const std::string& fragShader);
+
+    static GlExpected<ScopedGlProgram> MakeProgram(GlDispatch& dispatch, GLenum programBinaryFormat,
+                                                   const std::vector<uint8_t>& programBinaryData);
+
+    ScopedGlProgram(ScopedGlProgram&& rhs) : mGlDispatch(rhs.mGlDispatch), mHandle(rhs.mHandle) {
+        rhs.mHandle = 0;
+    }
+
+    ScopedGlProgram& operator=(ScopedGlProgram&& rhs) {
+        mGlDispatch = rhs.mGlDispatch;
+        std::swap(mHandle, rhs.mHandle);
+        return *this;
+    }
+
+    ~ScopedGlProgram() {
+        if (mHandle != 0) {
+            mGlDispatch->glDeleteProgram(mHandle);
+            mHandle = 0;
+        }
+    }
+
+    operator GLuint() { return mHandle; }
+    operator GLuint() const { return mHandle; }
+
+   private:
+    ScopedGlProgram(GlDispatch& dispatch, GLuint handle)
+        : mGlDispatch(&dispatch), mHandle(handle) {}
+
+    GlDispatch* mGlDispatch = nullptr;
+    GLuint mHandle = 0;
+};
+
+class ScopedAHardwareBuffer {
+   public:
+    ScopedAHardwareBuffer() = default;
+
+    static GlExpected<ScopedAHardwareBuffer> Allocate(Gralloc& gralloc, uint32_t width,
+                                                      uint32_t height, uint32_t format);
+
+    ScopedAHardwareBuffer(const ScopedAHardwareBuffer& rhs) = delete;
+    ScopedAHardwareBuffer& operator=(const ScopedAHardwareBuffer& rhs) = delete;
+
+    ScopedAHardwareBuffer(ScopedAHardwareBuffer&& rhs)
+        : mGralloc(rhs.mGralloc), mHandle(rhs.mHandle) {
+        rhs.mHandle = nullptr;
+    }
+
+    ScopedAHardwareBuffer& operator=(ScopedAHardwareBuffer&& rhs) {
+        mGralloc = rhs.mGralloc;
+        std::swap(mHandle, rhs.mHandle);
+        return *this;
+    }
+
+    ~ScopedAHardwareBuffer() {
+        if (mHandle != nullptr) {
+            mGralloc->release(mHandle);
+            mHandle = 0;
+        }
+    }
+
+    GlExpected<uint8_t*> Lock() {
+        uint8_t* mapped = nullptr;
+        int status = mGralloc->lock(mHandle, &mapped);
+        if (status != 0) {
+            return android::base::unexpected("Failed to lock AHB");
+        }
+        return mapped;
+    }
+
+    void Unlock() { mGralloc->unlock(mHandle); }
+
+    operator AHardwareBuffer*() { return mHandle; }
+    operator AHardwareBuffer*() const { return mHandle; }
+
+   private:
+    ScopedAHardwareBuffer(Gralloc& gralloc, AHardwareBuffer* handle)
+        : mGralloc(&gralloc), mHandle(handle) {}
+
+    Gralloc* mGralloc = nullptr;
+    AHardwareBuffer* mHandle = nullptr;
+};
+
 enum class GfxstreamTransport {
   kVirtioGpuAsg,
   kVirtioGpuPipe,
@@ -160,19 +391,7 @@
 std::string GetTestName(const ::testing::TestParamInfo<TestParams>& info);
 
 class GfxstreamEnd2EndTest : public ::testing::TestWithParam<TestParams> {
-  protected:
-    struct GuestGlDispatchTable {
-        #define DECLARE_EGL_FUNCTION(return_type, function_name, signature) \
-            return_type (*function_name) signature = nullptr;
-
-        #define DECLARE_GLES_FUNCTION(return_type, function_name, signature, args) \
-            return_type (*function_name) signature = nullptr;
-
-        LIST_RENDER_EGL_FUNCTIONS(DECLARE_EGL_FUNCTION)
-        LIST_RENDER_EGL_EXTENSIONS_FUNCTIONS(DECLARE_EGL_FUNCTION)
-        LIST_GLES_FUNCTIONS(DECLARE_GLES_FUNCTION, DECLARE_GLES_FUNCTION)
-    };
-
+   public:
     std::unique_ptr<GuestGlDispatchTable> SetupGuestGl();
     std::unique_ptr<vkhpp::DynamicLoader> SetupGuestVk();
 
@@ -193,10 +412,13 @@
                                       EGLContext context,
                                       EGLSurface surface);
 
-    GlExpected<GLuint> SetUpShader(GLenum type, const std::string& source);
+    GlExpected<ScopedGlShader> SetUpShader(GLenum type, const std::string& source);
 
-    GlExpected<GLuint> SetUpProgram(const std::string& vertSource,
-                                    const std::string& fragSource);
+    GlExpected<ScopedGlProgram> SetUpProgram(const std::string& vertSource,
+                                             const std::string& fragSource);
+
+    GlExpected<ScopedGlProgram> SetUpProgram(GLenum programBinaryFormat,
+                                             const std::vector<uint8_t>& programBinaryData);
 
     struct TypicalVkTestEnvironment {
         vkhpp::UniqueInstance instance;
diff --git a/common/utils/Android.bp b/common/utils/Android.bp
index 9d8040b..35d823b 100644
--- a/common/utils/Android.bp
+++ b/common/utils/Android.bp
@@ -1,4 +1,3 @@
-
 package {
     // See: http://go/android-license-faq
     default_applicable_licenses: ["hardware_google_gfxstream_license"],
diff --git a/guest/GLESv2_enc/GL2Encoder.cpp b/guest/GLESv2_enc/GL2Encoder.cpp
index 937a18a..7806906 100755
--- a/guest/GLESv2_enc/GL2Encoder.cpp
+++ b/guest/GLESv2_enc/GL2Encoder.cpp
@@ -15,21 +15,21 @@
 */
 
 #include "GL2Encoder.h"
-#include "GLESv2Validation.h"
-#include "GLESTextureUtils.h"
-
-#include <string>
-#include <map>
-
-#include <assert.h>
-#include <ctype.h>
 
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
 #include <GLES2/gl2platform.h>
-
 #include <GLES3/gl3.h>
 #include <GLES3/gl31.h>
+#include <assert.h>
+#include <ctype.h>
+
+#include <map>
+#include <string>
+
+#include "EncoderDebug.h"
+#include "GLESTextureUtils.h"
+#include "GLESv2Validation.h"
 
 using gfxstream::guest::BufferData;
 using gfxstream::guest::ChecksumCalculator;
@@ -1939,23 +1939,11 @@
     ctx->glFinishRoundTrip(self);
 }
 
-void GL2Encoder::s_glLinkProgram(void * self, GLuint program)
-{
-    GL2Encoder *ctx = (GL2Encoder *)self;
-    bool isProgram = ctx->m_shared->isProgram(program);
-    SET_ERROR_IF(!isProgram && !ctx->m_shared->isShader(program), GL_INVALID_VALUE);
-    SET_ERROR_IF(!isProgram, GL_INVALID_OPERATION);
-
-    if (program == ctx->m_state->currentProgram() ||
-        (!ctx->m_state->currentProgram() &&
-         (program == ctx->m_state->currentShaderProgram()))) {
-        SET_ERROR_IF(ctx->m_state->getTransformFeedbackActive(), GL_INVALID_OPERATION);
-    }
-
-    ctx->m_glLinkProgram_enc(self, program);
+void GL2Encoder::updateProgramInfoAfterLink(GLuint program) {
+    GL2Encoder* ctx = this;
 
     GLint linkStatus = 0;
-    ctx->m_glGetProgramiv_enc(self, program, GL_LINK_STATUS, &linkStatus);
+    ctx->m_glGetProgramiv_enc(ctx, program, GL_LINK_STATUS, &linkStatus);
     ctx->m_shared->setProgramLinkStatus(program, linkStatus);
     if (!linkStatus) {
         return;
@@ -1964,15 +1952,15 @@
     // get number of active uniforms and attributes in the program
     GLint numUniforms=0;
     GLint numAttributes=0;
-    ctx->m_glGetProgramiv_enc(self, program, GL_ACTIVE_UNIFORMS, &numUniforms);
-    ctx->m_glGetProgramiv_enc(self, program, GL_ACTIVE_ATTRIBUTES, &numAttributes);
+    ctx->m_glGetProgramiv_enc(ctx, program, GL_ACTIVE_UNIFORMS, &numUniforms);
+    ctx->m_glGetProgramiv_enc(ctx, program, GL_ACTIVE_ATTRIBUTES, &numAttributes);
     ctx->m_shared->initProgramData(program,numUniforms,numAttributes);
 
     //get the length of the longest uniform name
     GLint maxLength=0;
     GLint maxAttribLength=0;
-    ctx->m_glGetProgramiv_enc(self, program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxLength);
-    ctx->m_glGetProgramiv_enc(self, program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxAttribLength);
+    ctx->m_glGetProgramiv_enc(ctx, program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxLength);
+    ctx->m_glGetProgramiv_enc(ctx, program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxAttribLength);
 
     GLint size;
     GLenum type;
@@ -1982,14 +1970,14 @@
     //for each active uniform, get its size and starting location.
     for (GLint i=0 ; i<numUniforms ; ++i)
     {
-        ctx->m_glGetActiveUniform_enc(self, program, i, maxLength, NULL, &size, &type, name);
-        location = ctx->m_glGetUniformLocation_enc(self, program, name);
+        ctx->m_glGetActiveUniform_enc(ctx, program, i, maxLength, NULL, &size, &type, name);
+        location = ctx->m_glGetUniformLocation_enc(ctx, program, name);
         ctx->m_shared->setProgramIndexInfo(program, i, location, size, type, name);
     }
 
     for (GLint i = 0; i < numAttributes; ++i) {
-        ctx->m_glGetActiveAttrib_enc(self, program, i, maxAttribLength,  NULL, &size, &type, name);
-        location = ctx->m_glGetAttribLocation_enc(self, program, name);
+        ctx->m_glGetActiveAttrib_enc(ctx, program, i, maxAttribLength, NULL, &size, &type, name);
+        location = ctx->m_glGetAttribLocation_enc(ctx, program, name);
         ctx->m_shared->setProgramAttribInfo(program, i, location, size, type, name);
     }
 
@@ -2006,6 +1994,22 @@
     delete[] name;
 }
 
+void GL2Encoder::s_glLinkProgram(void* self, GLuint program) {
+    GL2Encoder* ctx = (GL2Encoder*)self;
+    bool isProgram = ctx->m_shared->isProgram(program);
+    SET_ERROR_IF(!isProgram && !ctx->m_shared->isShader(program), GL_INVALID_VALUE);
+    SET_ERROR_IF(!isProgram, GL_INVALID_OPERATION);
+
+    if (program == ctx->m_state->currentProgram() ||
+        (!ctx->m_state->currentProgram() && (program == ctx->m_state->currentShaderProgram()))) {
+        SET_ERROR_IF(ctx->m_state->getTransformFeedbackActive(), GL_INVALID_OPERATION);
+    }
+
+    ctx->m_glLinkProgram_enc(self, program);
+
+    ctx->updateProgramInfoAfterLink(program);
+}
+
 #define VALIDATE_PROGRAM_NAME(program) \
     bool isShaderOrProgramObject = \
         ctx->m_shared->isShaderOrProgramObject(program); \
@@ -6355,6 +6359,8 @@
     SET_ERROR_IF(~0 == binaryFormat, GL_INVALID_ENUM);
 
     ctx->m_glProgramBinary_enc(self, program, binaryFormat, binary, length);
+
+    ctx->updateProgramInfoAfterLink(program);
 }
 
 void GL2Encoder::s_glGetSamplerParameterfv(void *self, GLuint sampler, GLenum pname, GLfloat* params) {
diff --git a/guest/GLESv2_enc/GL2Encoder.h b/guest/GLESv2_enc/GL2Encoder.h
index 8c364a8..b1fde1b 100644
--- a/guest/GLESv2_enc/GL2Encoder.h
+++ b/guest/GLESv2_enc/GL2Encoder.h
@@ -43,32 +43,20 @@
 
 class GL2Encoder : public gl2_encoder_context_t {
 public:
-    GL2Encoder(gfxstream::guest::IOStream *stream, 
-               gfxstream::guest::ChecksumCalculator* protocol);
-    virtual ~GL2Encoder();
-    const Extensions& getExtensions() const { return m_extensions; }
-    void setDrawCallFlushInterval(uint32_t interval) {
-        m_drawCallFlushInterval = interval;
-    }
-    void setHasAsyncUnmapBuffer(int version) {
-        m_hasAsyncUnmapBuffer = version;
-    }
-    void setHasSyncBufferData(bool value) {
-        m_hasSyncBufferData = value;
-    }
-    void setNoHostError(bool noHostError) {
-        m_noHostError = noHostError;
-    }
-    void setClientState(gfxstream::guest::GLClientState *state) {
-        m_state = state;
-    }
-    void setVersion(int major, int minor,
-                    int deviceMajor, int deviceMinor) {
-        m_currMajorVersion = major;
-        m_currMinorVersion = minor;
-        m_deviceMajorVersion = deviceMajor;
-        m_deviceMinorVersion = deviceMinor;
-    }
+ GL2Encoder(gfxstream::guest::IOStream* stream, gfxstream::guest::ChecksumCalculator* protocol);
+ virtual ~GL2Encoder();
+ const Extensions& getExtensions() const { return m_extensions; }
+ void setDrawCallFlushInterval(uint32_t interval) { m_drawCallFlushInterval = interval; }
+ void setHasAsyncUnmapBuffer(int version) { m_hasAsyncUnmapBuffer = version; }
+ void setHasSyncBufferData(bool value) { m_hasSyncBufferData = value; }
+ void setNoHostError(bool noHostError) { m_noHostError = noHostError; }
+ void setClientState(gfxstream::guest::GLClientState* state) { m_state = state; }
+ void setVersion(int major, int minor, int deviceMajor, int deviceMinor) {
+     m_currMajorVersion = major;
+     m_currMinorVersion = minor;
+     m_deviceMajorVersion = deviceMajor;
+     m_deviceMinorVersion = deviceMinor;
+ }
     void setClientStateMakeCurrent(gfxstream::guest::GLClientState *state,
                                    int majorVersion,
                                    int minorVersion,
@@ -202,6 +190,8 @@
     void updateHostTexture2DBindingsFromProgramData(GLuint program);
     bool texture2DNeedsOverride(GLenum target) const;
 
+    void updateProgramInfoAfterLink(GLuint program);
+
     // Utility classes for safe queries that
     // need access to private class members
     class ErrorUpdater;
diff --git a/guest/android/GrallocEmulated.cpp b/guest/android/GrallocEmulated.cpp
index e6031b1..d171486 100644
--- a/guest/android/GrallocEmulated.cpp
+++ b/guest/android/GrallocEmulated.cpp
@@ -41,7 +41,7 @@
 std::optional<uint32_t> DrmToVirglFormat(uint32_t drmFormat) {
     switch (drmFormat) {
         case DRM_FORMAT_ABGR8888:
-            return VIRGL_FORMAT_B8G8R8A8_UNORM;
+            return VIRGL_FORMAT_R8G8B8A8_UNORM;
         case DRM_FORMAT_BGR888:
             return VIRGL_FORMAT_R8G8B8_UNORM;
         case DRM_FORMAT_BGR565:
@@ -91,6 +91,33 @@
     }
 }
 
+int EmulatedAHardwareBuffer::lock(uint8_t** ptr) {
+    if (!mMapped) {
+        mMapped = mResource->createMapping();
+        if (!mMapped) {
+            ALOGE("Failed to lock EmulatedAHardwareBuffer: failed to create mapping.");
+            return -1;
+        }
+
+        mResource->transferFromHost(0, 0, mWidth, mHeight);
+        mResource->wait();
+    }
+
+    *ptr = (*mMapped)->asRawPtr();
+    return 0;
+}
+
+int EmulatedAHardwareBuffer::unlock() {
+    if (!mMapped) {
+        ALOGE("Failed to unlock EmulatedAHardwareBuffer: never locked?");
+        return -1;
+    }
+    mResource->transferToHost(0, 0, mWidth, mHeight);
+    mResource->wait();
+    mMapped.reset();
+    return 0;
+}
+
 EmulatedGralloc::EmulatedGralloc() {}
 
 uint32_t EmulatedGralloc::createColorBuffer(void*, int width, int height, uint32_t glFormat) {
@@ -152,6 +179,16 @@
     rahb->release();
 }
 
+int EmulatedGralloc::lock(AHardwareBuffer* ahb, uint8_t** ptr) {
+    auto* rahb = reinterpret_cast<EmulatedAHardwareBuffer*>(ahb);
+    return rahb->lock(ptr);
+}
+
+int EmulatedGralloc::unlock(AHardwareBuffer* ahb) {
+    auto* rahb = reinterpret_cast<EmulatedAHardwareBuffer*>(ahb);
+    return rahb->unlock();
+}
+
 uint32_t EmulatedGralloc::getHostHandle(const native_handle_t* handle) {
     const auto* ahb = reinterpret_cast<const EmulatedAHardwareBuffer*>(handle);
     return ahb->getResourceId();
diff --git a/guest/android/GrallocEmulated.h b/guest/android/GrallocEmulated.h
index 8a48fef..cf1ba0f 100644
--- a/guest/android/GrallocEmulated.h
+++ b/guest/android/GrallocEmulated.h
@@ -14,10 +14,11 @@
 
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <vector>
 
-#include "gfxstream/guest/Gralloc.h"
 #include "VirtGpu.h"
+#include "gfxstream/guest/Gralloc.h"
 
 namespace gfxstream {
 
@@ -48,11 +49,15 @@
     void acquire();
     void release();
 
+    int lock(uint8_t** ptr);
+    int unlock();
+
    private:
     uint32_t mRefCount;
     uint32_t mWidth;
     uint32_t mHeight;
     VirtGpuBlobPtr mResource;
+    std::optional<VirtGpuBlobMappingPtr> mMapped;
 };
 
 class EmulatedGralloc : public Gralloc {
@@ -69,6 +74,9 @@
     void acquire(AHardwareBuffer* ahb) override;
     void release(AHardwareBuffer* ahb) override;
 
+    int lock(AHardwareBuffer* ahb, uint8_t** ptr) override;
+    int unlock(AHardwareBuffer* ahb) override;
+
     uint32_t getHostHandle(const native_handle_t* handle) override;
     uint32_t getHostHandle(const AHardwareBuffer* handle) override;
 
diff --git a/guest/android/GrallocGoldfish.cpp b/guest/android/GrallocGoldfish.cpp
index f07abde..ea5cdbf 100644
--- a/guest/android/GrallocGoldfish.cpp
+++ b/guest/android/GrallocGoldfish.cpp
@@ -43,6 +43,13 @@
 
 void GoldfishGralloc::release(AHardwareBuffer* ahb) { AHardwareBuffer_release(ahb); }
 
+int GoldfishGralloc::lock(AHardwareBuffer* ahb, uint8_t** ptr) {
+    return AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_RARELY, -1, nullptr,
+                                reinterpret_cast<void**>(ptr));
+}
+
+int GoldfishGralloc::unlock(AHardwareBuffer* ahb) { return AHardwareBuffer_unlock(ahb, nullptr); }
+
 uint32_t GoldfishGralloc::getHostHandle(native_handle_t const* handle) {
     return cb_handle_t::from(handle)->hostHandle;
 }
diff --git a/guest/android/GrallocGoldfish.h b/guest/android/GrallocGoldfish.h
index 560550f..f25eedf 100644
--- a/guest/android/GrallocGoldfish.h
+++ b/guest/android/GrallocGoldfish.h
@@ -28,6 +28,9 @@
     void acquire(AHardwareBuffer* ahb) override;
     void release(AHardwareBuffer* ahb) override;
 
+    int lock(AHardwareBuffer* ahb, uint8_t** ptr) override;
+    int unlock(AHardwareBuffer* ahb) override;
+
     uint32_t getHostHandle(native_handle_t const* handle) override;
     uint32_t getHostHandle(const AHardwareBuffer* handle) override;
 
diff --git a/guest/android/GrallocMinigbm.cpp b/guest/android/GrallocMinigbm.cpp
index a3daf43..85d7d9e 100644
--- a/guest/android/GrallocMinigbm.cpp
+++ b/guest/android/GrallocMinigbm.cpp
@@ -176,6 +176,13 @@
 
 void MinigbmGralloc::release(AHardwareBuffer* ahb) { AHardwareBuffer_release(ahb); }
 
+int MinigbmGralloc::lock(AHardwareBuffer* ahb, uint8_t** ptr) {
+    return AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_RARELY, -1, nullptr,
+                                reinterpret_cast<void**>(ptr));
+}
+
+int MinigbmGralloc::unlock(AHardwareBuffer* ahb) { return AHardwareBuffer_unlock(ahb, nullptr); }
+
 uint32_t MinigbmGralloc::getHostHandle(const native_handle_t* handle) {
     struct drm_virtgpu_resource_info info;
     if (!getVirtioGpuResourceInfo(m_fd, handle, &info)) {
diff --git a/guest/android/GrallocMinigbm.h b/guest/android/GrallocMinigbm.h
index c02aaeb..3ec1585 100644
--- a/guest/android/GrallocMinigbm.h
+++ b/guest/android/GrallocMinigbm.h
@@ -28,6 +28,9 @@
     void acquire(AHardwareBuffer* ahb) override;
     void release(AHardwareBuffer* ahb) override;
 
+    int lock(AHardwareBuffer* ahb, uint8_t** ptr) override;
+    int unlock(AHardwareBuffer* ahb) override;
+
     uint32_t getHostHandle(native_handle_t const* handle) override;
     uint32_t getHostHandle(const AHardwareBuffer* handle) override;
 
diff --git a/guest/android/include/gfxstream/guest/Gralloc.h b/guest/android/include/gfxstream/guest/Gralloc.h
index 5eb9139..ff281df 100644
--- a/guest/android/include/gfxstream/guest/Gralloc.h
+++ b/guest/android/include/gfxstream/guest/Gralloc.h
@@ -41,6 +41,9 @@
     virtual int allocate(uint32_t width, uint32_t height, uint32_t format, uint64_t usage,
                          AHardwareBuffer** outputAhb) = 0;
 
+    virtual int lock(AHardwareBuffer* ahb, uint8_t** ptr) = 0;
+    virtual int unlock(AHardwareBuffer* ahb) = 0;
+
     virtual const native_handle_t* getNativeHandle(const AHardwareBuffer* ahb) = 0;
 
     virtual uint32_t getHostHandle(const native_handle_t* handle) = 0;
diff --git a/guest/platform/include/VirtGpu.h b/guest/platform/include/VirtGpu.h
index db5e2af..b9e039a 100644
--- a/guest/platform/include/VirtGpu.h
+++ b/guest/platform/include/VirtGpu.h
@@ -141,8 +141,15 @@
     virtual VirtGpuBlobMappingPtr createMapping(void) = 0;
     virtual int exportBlob(struct VirtGpuExternalHandle& handle) = 0;
 
-    virtual int transferFromHost(uint32_t offset, uint32_t size) = 0;
-    virtual int transferToHost(uint32_t offset, uint32_t size) = 0;
+    virtual int transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) = 0;
+    virtual int transferFromHost(uint32_t offset, uint32_t size) {
+        return transferFromHost(offset, 0, size, 1);
+    }
+
+    virtual int transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) = 0;
+    virtual int transferToHost(uint32_t offset, uint32_t size) {
+        return transferToHost(offset, 0, size, 1);
+    }
 };
 
 class VirtGpuBlobMapping {
diff --git a/guest/platform/linux/LinuxVirtGpu.h b/guest/platform/linux/LinuxVirtGpu.h
index 6515728..8533280 100644
--- a/guest/platform/linux/LinuxVirtGpu.h
+++ b/guest/platform/linux/LinuxVirtGpu.h
@@ -31,8 +31,8 @@
     VirtGpuBlobMappingPtr createMapping(void) override;
     int exportBlob(struct VirtGpuExternalHandle& handle) override;
 
-    int transferFromHost(uint32_t offset, uint32_t size) override;
-    int transferToHost(uint32_t offset, uint32_t size) override;
+    int transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
+    int transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
 
    private:
     // Not owned.  Really should use a ScopedFD for this, but doesn't matter since we have a
diff --git a/guest/platform/linux/LinuxVirtGpuBlob.cpp b/guest/platform/linux/LinuxVirtGpuBlob.cpp
index 9ba1adc..bbf3d36 100644
--- a/guest/platform/linux/LinuxVirtGpuBlob.cpp
+++ b/guest/platform/linux/LinuxVirtGpuBlob.cpp
@@ -109,14 +109,14 @@
     return 0;
 }
 
-int LinuxVirtGpuBlob::transferToHost(uint32_t offset, uint32_t size) {
+int LinuxVirtGpuBlob::transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) {
     int ret;
     struct drm_virtgpu_3d_transfer_to_host xfer = {0};
 
-    xfer.box.x = offset;
-    xfer.box.y = 0;
-    xfer.box.w = size;
-    xfer.box.h = 1;
+    xfer.box.x = x;
+    xfer.box.y = y;
+    xfer.box.w = w;
+    xfer.box.h = h;
     xfer.box.d = 1;
     xfer.bo_handle = mBlobHandle;
 
@@ -129,14 +129,14 @@
     return 0;
 }
 
-int LinuxVirtGpuBlob::transferFromHost(uint32_t offset, uint32_t size) {
+int LinuxVirtGpuBlob::transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) {
     int ret;
     struct drm_virtgpu_3d_transfer_from_host xfer = {0};
 
-    xfer.box.x = offset;
-    xfer.box.y = 0;
-    xfer.box.w = size;
-    xfer.box.h = 1;
+    xfer.box.x = x;
+    xfer.box.y = y;
+    xfer.box.w = w;
+    xfer.box.h = h;
     xfer.box.d = 1;
     xfer.bo_handle = mBlobHandle;
 
diff --git a/guest/platform/rutabaga/RutabagaLayer.cpp b/guest/platform/rutabaga/RutabagaLayer.cpp
index eee5f10..f702e2b 100644
--- a/guest/platform/rutabaga/RutabagaLayer.cpp
+++ b/guest/platform/rutabaga/RutabagaLayer.cpp
@@ -101,8 +101,13 @@
 
     int TransferFromHost(uint32_t contextId, uint32_t resourceId, uint32_t transferOffset,
                          uint32_t transferSize);
+    int TransferFromHost(uint32_t contextId, uint32_t resourceId, uint32_t x, uint32_t y,
+                         uint32_t w, uint32_t h);
+
     int TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t transferOffset,
                        uint32_t transferSize);
+    int TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t x, uint32_t y, uint32_t w,
+                       uint32_t h);
 
     std::optional<uint32_t> CreateBlob(uint32_t contextId, uint32_t blobMem, uint32_t blobFlags,
                                        uint64_t blobId, uint64_t blobSize);
@@ -159,14 +164,18 @@
     struct VirtioGpuTaskTransferToHost {
         uint32_t contextId;
         uint32_t resourceId;
-        uint32_t transferOffset;
-        uint32_t transferSize;
+        uint32_t x;
+        uint32_t y;
+        uint32_t w;
+        uint32_t h;
     };
     struct VirtioGpuTaskTransferFromHost {
         uint32_t contextId;
         uint32_t resourceId;
-        uint32_t transferOffset;
-        uint32_t transferSize;
+        uint32_t x;
+        uint32_t y;
+        uint32_t w;
+        uint32_t h;
     };
     struct VirtioGpuTaskUnrefResource {
         uint32_t resourceId;
@@ -458,6 +467,12 @@
                                                                uint32_t resourceId,
                                                                uint32_t transferOffset,
                                                                uint32_t transferSize) {
+    return TransferFromHost(contextId, resourceId, transferOffset, 0, transferSize, 1);
+}
+
+int EmulatedVirtioGpu::EmulatedVirtioGpuImpl::TransferFromHost(uint32_t contextId,
+                                                               uint32_t resourceId, uint32_t x,
+                                                               uint32_t y, uint32_t w, uint32_t h) {
     EmulatedResource* resource = GetResource(resourceId);
     if (resource == nullptr) {
         ALOGE("Failed to TransferFromHost() on resource %" PRIu32 ": not found.", resourceId);
@@ -467,8 +482,10 @@
     VirtioGpuTaskTransferFromHost task = {
         .contextId = contextId,
         .resourceId = resourceId,
-        .transferOffset = transferOffset,
-        .transferSize = transferSize,
+        .x = x,
+        .y = y,
+        .w = w,
+        .h = h,
     };
     auto waitable = EnqueueVirtioGpuTask(contextId, std::move(task));
 
@@ -484,6 +501,12 @@
                                                              uint32_t resourceId,
                                                              uint32_t transferOffset,
                                                              uint32_t transferSize) {
+    return TransferToHost(contextId, resourceId, transferOffset, 0, transferSize, 1);
+}
+
+int EmulatedVirtioGpu::EmulatedVirtioGpuImpl::TransferToHost(uint32_t contextId,
+                                                             uint32_t resourceId, uint32_t x,
+                                                             uint32_t y, uint32_t w, uint32_t h) {
     EmulatedResource* resource = GetResource(resourceId);
     if (resource == nullptr) {
         ALOGE("Failed to TransferFromHost() on resource %" PRIu32 ": not found.", resourceId);
@@ -493,8 +516,10 @@
     VirtioGpuTaskTransferToHost task = {
         .contextId = contextId,
         .resourceId = resourceId,
-        .transferOffset = transferOffset,
-        .transferSize = transferSize,
+        .x = x,
+        .y = y,
+        .w = w,
+        .h = h,
     };
     auto waitable = EnqueueVirtioGpuTask(contextId, std::move(task));
 
@@ -800,7 +825,7 @@
     }
 
     resource->iovec.iov_base = task.resourceBytes;
-    resource->iovec.iov_len = task.params.width;
+    resource->iovec.iov_len = task.params.width * task.params.height * 4;
 
     struct rutabaga_iovecs vecs = {0};
     vecs.iovecs = &resource->iovec;
@@ -864,9 +889,11 @@
 
 void EmulatedVirtioGpu::EmulatedVirtioGpuImpl::DoTask(VirtioGpuTaskTransferFromHost task) {
     struct rutabaga_transfer transfer = {0};
-    transfer.x = task.transferOffset;
-    transfer.w = task.transferSize;
-    transfer.h = 1;
+    transfer.x = task.x;
+    transfer.y = task.y;
+    transfer.z = 0;
+    transfer.w = task.w;
+    transfer.h = task.h;
     transfer.d = 1;
 
     int ret = rutabaga_resource_transfer_read(mRutabaga, task.contextId, task.resourceId, &transfer,
@@ -878,9 +905,11 @@
 
 void EmulatedVirtioGpu::EmulatedVirtioGpuImpl::DoTask(VirtioGpuTaskTransferToHost task) {
     struct rutabaga_transfer transfer = {0};
-    transfer.x = task.transferOffset;
-    transfer.w = task.transferSize;
-    transfer.h = 1;
+    transfer.x = task.x;
+    transfer.y = task.y;
+    transfer.z = 0;
+    transfer.w = task.w;
+    transfer.h = task.h;
     transfer.d = 1;
 
     int ret =
@@ -998,7 +1027,7 @@
     instance = std::shared_ptr<EmulatedVirtioGpu>(new EmulatedVirtioGpu());
 
     bool withGl = false;
-    bool withVk = true;
+    bool withVk = false;
     bool withVkSnapshots = false;
 
     struct Option {
@@ -1065,11 +1094,21 @@
     return mImpl->TransferFromHost(contextId, resourceId, offset, size);
 }
 
+int EmulatedVirtioGpu::TransferFromHost(uint32_t contextId, uint32_t resourceId, uint32_t x,
+                                        uint32_t y, uint32_t w, uint32_t h) {
+    return mImpl->TransferFromHost(contextId, resourceId, x, y, w, h);
+}
+
 int EmulatedVirtioGpu::TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t offset,
                                       uint32_t size) {
     return mImpl->TransferToHost(contextId, resourceId, offset, size);
 }
 
+int EmulatedVirtioGpu::TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t x,
+                                      uint32_t y, uint32_t w, uint32_t h) {
+    return mImpl->TransferToHost(contextId, resourceId, x, y, w, h);
+}
+
 std::optional<uint32_t> EmulatedVirtioGpu::CreateBlob(uint32_t contextId, uint32_t blobMem,
                                                       uint32_t blobFlags, uint64_t blobId,
                                                       uint64_t blobSize) {
diff --git a/guest/platform/rutabaga/RutabagaLayer.h b/guest/platform/rutabaga/RutabagaLayer.h
index d5a0032..9303a64 100644
--- a/guest/platform/rutabaga/RutabagaLayer.h
+++ b/guest/platform/rutabaga/RutabagaLayer.h
@@ -68,8 +68,13 @@
 
    int TransferFromHost(uint32_t contextId, uint32_t resourceId, uint32_t transferOffset,
                         uint32_t transferSize);
+   int TransferFromHost(uint32_t contextId, uint32_t resourceId, uint32_t x, uint32_t y, uint32_t w,
+                        uint32_t h);
+
    int TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t transferOffset,
                       uint32_t transferSize);
+   int TransferToHost(uint32_t contextId, uint32_t resourceId, uint32_t x, uint32_t y, uint32_t w,
+                      uint32_t h);
 
    void SignalEmulatedFence(int fenceId);
 
diff --git a/guest/platform/rutabaga/RutabagaVirtGpu.h b/guest/platform/rutabaga/RutabagaVirtGpu.h
index a47f60a..2e605e6 100644
--- a/guest/platform/rutabaga/RutabagaVirtGpu.h
+++ b/guest/platform/rutabaga/RutabagaVirtGpu.h
@@ -53,9 +53,12 @@
     int wait() override;
 
     int transferFromHost(uint32_t offset, uint32_t size) override;
-    int transferToHost(uint32_t offset, uint32_t size) override;
+    int transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
 
-  private:
+    int transferToHost(uint32_t offset, uint32_t size) override;
+    int transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
+
+   private:
     friend class RutabagaVirtGpuDevice;
 
     enum class ResourceType {
diff --git a/guest/platform/rutabaga/RutabagaVirtGpuBlob.cpp b/guest/platform/rutabaga/RutabagaVirtGpuBlob.cpp
index 6f97ffb..d9392f0 100644
--- a/guest/platform/rutabaga/RutabagaVirtGpuBlob.cpp
+++ b/guest/platform/rutabaga/RutabagaVirtGpuBlob.cpp
@@ -71,6 +71,15 @@
     return mEmulation->TransferFromHost(mContextId, mResourceId, offset, size);
 }
 
+int RutabagaVirtGpuResource::transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) {
+    if (mResourceType != ResourceType::kPipe) {
+        ALOGE("Unexpected transferFromHost() called on non-pipe resource.");
+        return -1;
+    }
+
+    return mEmulation->TransferFromHost(mContextId, mResourceId, x, y, w, h);
+}
+
 int RutabagaVirtGpuResource::transferToHost(uint32_t offset, uint32_t size) {
     if (mResourceType != ResourceType::kPipe) {
         ALOGE("Unexpected transferToHost() called on non-pipe resource.");
@@ -80,4 +89,13 @@
     return mEmulation->TransferToHost(mContextId, mResourceId, offset, size);
 }
 
+int RutabagaVirtGpuResource::transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) {
+    if (mResourceType != ResourceType::kPipe) {
+        ALOGE("Unexpected transferToHost() called on non-pipe resource.");
+        return -1;
+    }
+
+    return mEmulation->TransferToHost(mContextId, mResourceId, x, y, w, h);
+}
+
 }  // namespace gfxstream
diff --git a/guest/platform/stub/StubVirtGpu.h b/guest/platform/stub/StubVirtGpu.h
index 7466cb5..6a00a4a 100644
--- a/guest/platform/stub/StubVirtGpu.h
+++ b/guest/platform/stub/StubVirtGpu.h
@@ -30,10 +30,10 @@
     VirtGpuBlobMappingPtr createMapping(void) override;
     int exportBlob(struct VirtGpuExternalHandle& handle) override;
 
-    int transferFromHost(uint32_t offset, uint32_t size) override;
-    int transferToHost(uint32_t offset, uint32_t size) override;
+    int transferFromHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
+    int transferToHost(uint32_t x, uint32_t y, uint32_t w, uint32_t h) override;
 
-  private:
+   private:
     // Not owned.  Really should use a ScopedFD for this, but doesn't matter since we have a
     // singleton deviceimplemenentation anyways.
     int64_t mDeviceHandle;
diff --git a/guest/platform/stub/StubVirtGpuBlob.cpp b/guest/platform/stub/StubVirtGpuBlob.cpp
index 4e8c23c..fb4c789 100644
--- a/guest/platform/stub/StubVirtGpuBlob.cpp
+++ b/guest/platform/stub/StubVirtGpuBlob.cpp
@@ -37,10 +37,6 @@
     return -1;
 }
 
-int StubVirtGpuBlob::transferFromHost(uint32_t, uint32_t) {
-    return -1;
-}
+int StubVirtGpuBlob::transferFromHost(uint32_t, uint32_t, uint32_t, uint32_t) { return -1; }
 
-int StubVirtGpuBlob::transferToHost(uint32_t, uint32_t) {
-    return -1;
-}
+int StubVirtGpuBlob::transferToHost(uint32_t, uint32_t, uint32_t, uint32_t) { return -1; }
diff --git a/host/features/Android.bp b/host/features/Android.bp
index 8326658..7ebc24a 100644
--- a/host/features/Android.bp
+++ b/host/features/Android.bp
@@ -1,4 +1,3 @@
-
 package {
     // See: http://go/android-license-faq
     default_applicable_licenses: ["hardware_google_gfxstream_license"],