C++程序  |  559行  |  19.52 KB

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <system/window.h>

#include <sys/types.h>
#include <sys/resource.h>
#include <sched.h>

#define EGL_EGLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES/gl.h>
#include <GLES/glext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

#include <string.h>

#include "rsdCore.h"
#include "rsdGL.h"

#include <malloc.h>
#include "rsContext.h"
#include "rsDevice.h"
#include "rsdShaderCache.h"
#include "rsdVertexArray.h"
#include "rsdFrameBufferObj.h"

using android::renderscript::Context;

static int32_t gGLContextCount = 0;

static void checkEglError(const char* op, EGLBoolean returnVal = EGL_TRUE) {
    struct EGLUtils {
        static const char *strerror(EGLint err) {
            switch (err){
                case EGL_SUCCESS:           return "EGL_SUCCESS";
                case EGL_NOT_INITIALIZED:   return "EGL_NOT_INITIALIZED";
                case EGL_BAD_ACCESS:        return "EGL_BAD_ACCESS";
                case EGL_BAD_ALLOC:         return "EGL_BAD_ALLOC";
                case EGL_BAD_ATTRIBUTE:     return "EGL_BAD_ATTRIBUTE";
                case EGL_BAD_CONFIG:        return "EGL_BAD_CONFIG";
                case EGL_BAD_CONTEXT:       return "EGL_BAD_CONTEXT";
                case EGL_BAD_CURRENT_SURFACE: return "EGL_BAD_CURRENT_SURFACE";
                case EGL_BAD_DISPLAY:       return "EGL_BAD_DISPLAY";
                case EGL_BAD_MATCH:         return "EGL_BAD_MATCH";
                case EGL_BAD_NATIVE_PIXMAP: return "EGL_BAD_NATIVE_PIXMAP";
                case EGL_BAD_NATIVE_WINDOW: return "EGL_BAD_NATIVE_WINDOW";
                case EGL_BAD_PARAMETER:     return "EGL_BAD_PARAMETER";
                case EGL_BAD_SURFACE:       return "EGL_BAD_SURFACE";
                case EGL_CONTEXT_LOST:      return "EGL_CONTEXT_LOST";
                default: return "UNKNOWN";
            }
        }
    };

    if (returnVal != EGL_TRUE) {
        fprintf(stderr, "%s() returned %d\n", op, returnVal);
    }

    for (EGLint error = eglGetError(); error != EGL_SUCCESS; error
            = eglGetError()) {
        fprintf(stderr, "after %s() eglError %s (0x%x)\n", op, EGLUtils::strerror(error),
                error);
    }
}

static void printEGLConfiguration(EGLDisplay dpy, EGLConfig config) {

#define X(VAL) {VAL, #VAL}
    struct {EGLint attribute; const char* name;} names[] = {
    X(EGL_BUFFER_SIZE),
    X(EGL_ALPHA_SIZE),
    X(EGL_BLUE_SIZE),
    X(EGL_GREEN_SIZE),
    X(EGL_RED_SIZE),
    X(EGL_DEPTH_SIZE),
    X(EGL_STENCIL_SIZE),
    X(EGL_CONFIG_CAVEAT),
    X(EGL_CONFIG_ID),
    X(EGL_LEVEL),
    X(EGL_MAX_PBUFFER_HEIGHT),
    X(EGL_MAX_PBUFFER_PIXELS),
    X(EGL_MAX_PBUFFER_WIDTH),
    X(EGL_NATIVE_RENDERABLE),
    X(EGL_NATIVE_VISUAL_ID),
    X(EGL_NATIVE_VISUAL_TYPE),
    X(EGL_SAMPLES),
    X(EGL_SAMPLE_BUFFERS),
    X(EGL_SURFACE_TYPE),
    X(EGL_TRANSPARENT_TYPE),
    X(EGL_TRANSPARENT_RED_VALUE),
    X(EGL_TRANSPARENT_GREEN_VALUE),
    X(EGL_TRANSPARENT_BLUE_VALUE),
    X(EGL_BIND_TO_TEXTURE_RGB),
    X(EGL_BIND_TO_TEXTURE_RGBA),
    X(EGL_MIN_SWAP_INTERVAL),
    X(EGL_MAX_SWAP_INTERVAL),
    X(EGL_LUMINANCE_SIZE),
    X(EGL_ALPHA_MASK_SIZE),
    X(EGL_COLOR_BUFFER_TYPE),
    X(EGL_RENDERABLE_TYPE),
    X(EGL_CONFORMANT),
   };
#undef X

    for (size_t j = 0; j < sizeof(names) / sizeof(names[0]); j++) {
        EGLint value = -1;
        EGLBoolean returnVal = eglGetConfigAttrib(dpy, config, names[j].attribute, &value);
        if (returnVal) {
            ALOGV(" %s: %d (0x%x)", names[j].name, value, value);
        }
    }
}

static void DumpDebug(RsdHal *dc) {
    ALOGE(" EGL ver %i %i", dc->gl.egl.majorVersion, dc->gl.egl.minorVersion);
    ALOGE(" EGL context %p  surface %p,  Display=%p", dc->gl.egl.context, dc->gl.egl.surface,
         dc->gl.egl.display);
    ALOGE(" GL vendor: %s", dc->gl.gl.vendor);
    ALOGE(" GL renderer: %s", dc->gl.gl.renderer);
    ALOGE(" GL Version: %s", dc->gl.gl.version);
    ALOGE(" GL Extensions: %s", dc->gl.gl.extensions);
    ALOGE(" GL int Versions %i %i", dc->gl.gl.majorVersion, dc->gl.gl.minorVersion);

    ALOGV("MAX Textures %i, %i  %i", dc->gl.gl.maxVertexTextureUnits,
         dc->gl.gl.maxFragmentTextureImageUnits, dc->gl.gl.maxTextureImageUnits);
    ALOGV("MAX Attribs %i", dc->gl.gl.maxVertexAttribs);
    ALOGV("MAX Uniforms %i, %i", dc->gl.gl.maxVertexUniformVectors,
         dc->gl.gl.maxFragmentUniformVectors);
    ALOGV("MAX Varyings %i", dc->gl.gl.maxVaryingVectors);
}

void rsdGLShutdown(const Context *rsc) {
    RsdHal *dc = (RsdHal *)rsc->mHal.drv;

    rsdGLSetSurface(rsc, 0, 0, nullptr);
    dc->gl.shaderCache->cleanupAll();
    delete dc->gl.shaderCache;
    delete dc->gl.vertexArrayState;

    if (dc->gl.egl.context != EGL_NO_CONTEXT) {
        RSD_CALL_GL(eglMakeCurrent, dc->gl.egl.display,
                    EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        RSD_CALL_GL(eglDestroySurface, dc->gl.egl.display, dc->gl.egl.surfaceDefault);
        if (dc->gl.egl.surface != EGL_NO_SURFACE) {
            RSD_CALL_GL(eglDestroySurface, dc->gl.egl.display, dc->gl.egl.surface);
        }
        RSD_CALL_GL(eglDestroyContext, dc->gl.egl.display, dc->gl.egl.context);
        checkEglError("eglDestroyContext");
    }

    gGLContextCount--;
    if (!gGLContextCount) {
        RSD_CALL_GL(eglTerminate, dc->gl.egl.display);
    }
}

void getConfigData(const Context *rsc,
                   EGLint *configAttribs, size_t configAttribsLen,
                   uint32_t numSamples) {
    memset(configAttribs, 0, configAttribsLen*sizeof(*configAttribs));

    EGLint *configAttribsPtr = configAttribs;

    configAttribsPtr[0] = EGL_SURFACE_TYPE;
    configAttribsPtr[1] = EGL_PBUFFER_BIT;
    configAttribsPtr += 2;

    configAttribsPtr[0] = EGL_RENDERABLE_TYPE;
    configAttribsPtr[1] = EGL_OPENGL_ES2_BIT;
    configAttribsPtr += 2;

    configAttribsPtr[0] = EGL_RED_SIZE;
    configAttribsPtr[1] = 8;
    configAttribsPtr += 2;

    configAttribsPtr[0] = EGL_GREEN_SIZE;
    configAttribsPtr[1] = 8;
    configAttribsPtr += 2;

    configAttribsPtr[0] = EGL_BLUE_SIZE;
    configAttribsPtr[1] = 8;
    configAttribsPtr += 2;

    if (rsc->mUserSurfaceConfig.alphaMin > 0) {
        configAttribsPtr[0] = EGL_ALPHA_SIZE;
        configAttribsPtr[1] = rsc->mUserSurfaceConfig.alphaMin;
        configAttribsPtr += 2;
    }

    if (rsc->mUserSurfaceConfig.depthMin > 0) {
        configAttribsPtr[0] = EGL_DEPTH_SIZE;
        configAttribsPtr[1] = rsc->mUserSurfaceConfig.depthMin;
        configAttribsPtr += 2;
    }

    if (numSamples > 1) {
        configAttribsPtr[0] = EGL_SAMPLE_BUFFERS;
        configAttribsPtr[1] = 1;
        configAttribsPtr[2] = EGL_SAMPLES;
        configAttribsPtr[3] = numSamples;
        configAttribsPtr += 4;
    }

    configAttribsPtr[0] = EGL_NONE;
    rsAssert(configAttribsPtr < (configAttribs + configAttribsLen));
}

int32_t rsdGLInit(const Context *rsc) {
    RsdHal *dc = (RsdHal *)rsc->mHal.drv;

    dc->gl.egl.numConfigs = -1;

    EGLint configAttribs[128];
    EGLint context_attribs2[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };

    ALOGV("%p initEGL start", rsc);
    rsc->setWatchdogGL("eglGetDisplay", __LINE__, __FILE__);
    dc->gl.egl.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    checkEglError("eglGetDisplay");

    RSD_CALL_GL(eglInitialize, dc->gl.egl.display,
                &dc->gl.egl.majorVersion, &dc->gl.egl.minorVersion);
    checkEglError("eglInitialize");

    EGLBoolean ret;

    EGLint numConfigs = -1, n = 0;
    rsc->setWatchdogGL("eglChooseConfig", __LINE__, __FILE__);

    // Try minding a multisample config that matches the user request
    uint32_t minSample = rsc->mUserSurfaceConfig.samplesMin;
    uint32_t prefSample = rsc->mUserSurfaceConfig.samplesPref;
    for (uint32_t sampleCount = prefSample; sampleCount >= minSample; sampleCount--) {
        getConfigData(rsc, configAttribs, (sizeof(configAttribs) / sizeof(EGLint)), sampleCount);
        ret = eglChooseConfig(dc->gl.egl.display, configAttribs, 0, 0, &numConfigs);
        checkEglError("eglGetConfigs", ret);
        if (numConfigs > 0) {
            break;
        }
    }

    if (numConfigs) {
        EGLConfig* const configs = new EGLConfig[numConfigs];

        rsc->setWatchdogGL("eglChooseConfig", __LINE__, __FILE__);
        ret = eglChooseConfig(dc->gl.egl.display,
                configAttribs, configs, numConfigs, &n);
        if (!ret || !n) {
            checkEglError("eglChooseConfig", ret);
            ALOGE("%p, couldn't find an EGLConfig matching the screen format\n", rsc);
        }

        // The first config is guaranteed to over-satisfy the constraints
        dc->gl.egl.config = configs[0];

        // go through the list and skip configs that over-satisfy our needs
        for (int i=0 ; i<n ; i++) {
            if (rsc->mUserSurfaceConfig.alphaMin <= 0) {
                EGLint alphaSize;
                eglGetConfigAttrib(dc->gl.egl.display,
                        configs[i], EGL_ALPHA_SIZE, &alphaSize);
                if (alphaSize > 0) {
                    continue;
                }
            }

            if (rsc->mUserSurfaceConfig.depthMin <= 0) {
                EGLint depthSize;
                eglGetConfigAttrib(dc->gl.egl.display,
                        configs[i], EGL_DEPTH_SIZE, &depthSize);
                if (depthSize > 0) {
                    continue;
                }
            }

            // Found one!
            dc->gl.egl.config = configs[i];
            break;
        }

        delete [] configs;
    }

    //if (props.mLogVisual) {
    if (0) {
        printEGLConfiguration(dc->gl.egl.display, dc->gl.egl.config);
    }
    //}

    rsc->setWatchdogGL("eglCreateContext", __LINE__, __FILE__);
    dc->gl.egl.context = eglCreateContext(dc->gl.egl.display, dc->gl.egl.config,
                                          EGL_NO_CONTEXT, context_attribs2);
    checkEglError("eglCreateContext");
    if (dc->gl.egl.context == EGL_NO_CONTEXT) {
        ALOGE("%p, eglCreateContext returned EGL_NO_CONTEXT", rsc);
        rsc->setWatchdogGL(nullptr, 0, nullptr);
        return -1;
    }
    gGLContextCount++;

    EGLint pbuffer_attribs[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE };
    rsc->setWatchdogGL("eglCreatePbufferSurface", __LINE__, __FILE__);
    dc->gl.egl.surfaceDefault = eglCreatePbufferSurface(dc->gl.egl.display, dc->gl.egl.config,
            pbuffer_attribs);
    checkEglError("eglCreatePbufferSurface");
    if (dc->gl.egl.surfaceDefault == EGL_NO_SURFACE) {
        ALOGE("eglCreatePbufferSurface returned EGL_NO_SURFACE");
        rsdGLShutdown(rsc);
        rsc->setWatchdogGL(nullptr, 0, nullptr);
        return -1;
    }

    rsc->setWatchdogGL("eglMakeCurrent", __LINE__, __FILE__);
    ret = eglMakeCurrent(dc->gl.egl.display, dc->gl.egl.surfaceDefault,
                         dc->gl.egl.surfaceDefault, dc->gl.egl.context);
    if (ret == EGL_FALSE) {
        ALOGE("eglMakeCurrent returned EGL_FALSE");
        checkEglError("eglMakeCurrent", ret);
        rsdGLShutdown(rsc);
        rsc->setWatchdogGL(nullptr, 0, nullptr);
        return -1;
    }

    dc->gl.gl.version = glGetString(GL_VERSION);
    dc->gl.gl.vendor = glGetString(GL_VENDOR);
    dc->gl.gl.renderer = glGetString(GL_RENDERER);
    dc->gl.gl.extensions = glGetString(GL_EXTENSIONS);

    //ALOGV("EGL Version %i %i", mEGL.mMajorVersion, mEGL.mMinorVersion);
    //ALOGV("GL Version %s", mGL.mVersion);
    //ALOGV("GL Vendor %s", mGL.mVendor);
    //ALOGV("GL Renderer %s", mGL.mRenderer);
    //ALOGV("GL Extensions %s", mGL.mExtensions);

    const char *verptr = nullptr;
    if (strlen((const char *)dc->gl.gl.version) > 9) {
        if (!memcmp(dc->gl.gl.version, "OpenGL ES-CM", 12)) {
            verptr = (const char *)dc->gl.gl.version + 12;
        }
        if (!memcmp(dc->gl.gl.version, "OpenGL ES ", 10)) {
            verptr = (const char *)dc->gl.gl.version + 9;
        }
    }

    if (!verptr) {
        ALOGE("Error, OpenGL ES Lite not supported");
        rsdGLShutdown(rsc);
        rsc->setWatchdogGL(nullptr, 0, nullptr);
        return -1;
    } else {
        sscanf(verptr, " %i.%i", &dc->gl.gl.majorVersion, &dc->gl.gl.minorVersion);
    }

    glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &dc->gl.gl.maxVertexAttribs);
    glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS, &dc->gl.gl.maxVertexUniformVectors);
    glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &dc->gl.gl.maxVertexTextureUnits);

    glGetIntegerv(GL_MAX_VARYING_VECTORS, &dc->gl.gl.maxVaryingVectors);
    glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &dc->gl.gl.maxTextureImageUnits);

    glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &dc->gl.gl.maxFragmentTextureImageUnits);
    glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS, &dc->gl.gl.maxFragmentUniformVectors);

    dc->gl.gl.OES_texture_npot = nullptr != strstr((const char *)dc->gl.gl.extensions,
                                                "GL_OES_texture_npot");
    dc->gl.gl.IMG_texture_npot = nullptr != strstr((const char *)dc->gl.gl.extensions,
                                                   "GL_IMG_texture_npot");
    dc->gl.gl.NV_texture_npot_2D_mipmap = nullptr != strstr((const char *)dc->gl.gl.extensions,
                                                            "GL_NV_texture_npot_2D_mipmap");
    dc->gl.gl.EXT_texture_max_aniso = 1.0f;
    bool hasAniso = nullptr != strstr((const char *)dc->gl.gl.extensions,
                                   "GL_EXT_texture_filter_anisotropic");
    if (hasAniso) {
        glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &dc->gl.gl.EXT_texture_max_aniso);
    }

    if (0) {
        DumpDebug(dc);
    }

    dc->gl.shaderCache = new RsdShaderCache();
    dc->gl.vertexArrayState = new RsdVertexArrayState();
    dc->gl.vertexArrayState->init(dc->gl.gl.maxVertexAttribs);
    dc->gl.currentFrameBuffer = nullptr;
    dc->mHasGraphics = true;

    int syncFd = -1;
    // Create a EGL sync object.
    EGLSyncKHR sync = eglCreateSyncKHR(dc->gl.egl.display, EGL_SYNC_NATIVE_FENCE_ANDROID, NULL);
    if (sync != EGL_NO_SYNC_KHR) {
        // native fence fd will not be populated until flush() is done.
        glFlush();
        // Convert the EGL sync object to a file descriptor.
        syncFd = eglDupNativeFenceFDANDROID(dc->gl.egl.display, sync);
        if (syncFd == EGL_NO_NATIVE_FENCE_FD_ANDROID) {
            ALOGW("Failed to dup sync khr object");
            syncFd = -1;
        }
        // The sync object is no longer needed once we have the file descriptor.
        eglDestroySyncKHR(dc->gl.egl.display, sync);
    }

    ALOGV("%p initGLThread end", rsc);
    rsc->setWatchdogGL(nullptr, 0, nullptr);
    return syncFd;
}


bool rsdGLSetInternalSurface(const Context *rsc, RsNativeWindow sur) {
    RsdHal *dc = (RsdHal *)rsc->mHal.drv;

    EGLBoolean ret;
    if (dc->gl.egl.surface != nullptr) {
        rsc->setWatchdogGL("eglMakeCurrent", __LINE__, __FILE__);
        ret = eglMakeCurrent(dc->gl.egl.display, dc->gl.egl.surfaceDefault,
                             dc->gl.egl.surfaceDefault, dc->gl.egl.context);
        checkEglError("eglMakeCurrent", ret);

        rsc->setWatchdogGL("eglDestroySurface", __LINE__, __FILE__);
        ret = eglDestroySurface(dc->gl.egl.display, dc->gl.egl.surface);
        checkEglError("eglDestroySurface", ret);

        dc->gl.egl.surface = nullptr;
    }

    if (dc->gl.currentWndSurface != nullptr) {
        dc->gl.currentWndSurface->decStrong(nullptr);
    }

    dc->gl.currentWndSurface = (ANativeWindow *)sur;
    if (dc->gl.currentWndSurface != nullptr) {
        dc->gl.currentWndSurface->incStrong(nullptr);

        rsc->setWatchdogGL("eglCreateWindowSurface", __LINE__, __FILE__);
        dc->gl.egl.surface = eglCreateWindowSurface(dc->gl.egl.display, dc->gl.egl.config,
                                                    dc->gl.currentWndSurface, nullptr);
        checkEglError("eglCreateWindowSurface");
        if (dc->gl.egl.surface == EGL_NO_SURFACE) {
            ALOGE("eglCreateWindowSurface returned EGL_NO_SURFACE");
        }

        rsc->setWatchdogGL("eglMakeCurrent", __LINE__, __FILE__);
        ret = eglMakeCurrent(dc->gl.egl.display, dc->gl.egl.surface,
                             dc->gl.egl.surface, dc->gl.egl.context);
        checkEglError("eglMakeCurrent", ret);
    }
    rsc->setWatchdogGL(nullptr, 0, nullptr);
    return true;
}

bool rsdGLSetSurface(const Context *rsc, uint32_t w, uint32_t h, RsNativeWindow sur) {
    RsdHal *dc = (RsdHal *)rsc->mHal.drv;

    if (dc->gl.wndSurface != nullptr) {
        dc->gl.wndSurface->decStrong(nullptr);
        dc->gl.wndSurface = nullptr;
    }
    if(w && h) {
        // WAR: Some drivers fail to handle 0 size surfaces correctly. Use the
        // pbuffer to avoid this pitfall.
        dc->gl.wndSurface = (ANativeWindow *)sur;
        if (dc->gl.wndSurface != nullptr) {
            dc->gl.wndSurface->incStrong(nullptr);
        }
    }

    return rsdGLSetInternalSurface(rsc, sur);
}

void rsdGLSwap(const android::renderscript::Context *rsc) {
    RsdHal *dc = (RsdHal *)rsc->mHal.drv;
    RSD_CALL_GL(eglSwapBuffers, dc->gl.egl.display, dc->gl.egl.surface);
}

void rsdGLSetPriority(const Context *rsc, int32_t priority) {
    if (priority > 0) {
        // Mark context as low priority.
        ALOGV("low pri");
    } else {
        ALOGV("normal pri");
    }
}

void rsdGLCheckError(const android::renderscript::Context *rsc,
                     const char *msg, bool isFatal) {
    GLenum err = glGetError();
    if (err != GL_NO_ERROR) {
        char buf[1024];
        snprintf(buf, sizeof(buf), "GL Error = 0x%08x, from: %s", err, msg);

        if (isFatal) {
            rsc->setError(RS_ERROR_FATAL_DRIVER, buf);
        } else {
            switch (err) {
            case GL_OUT_OF_MEMORY:
                rsc->setError(RS_ERROR_OUT_OF_MEMORY, buf);
                break;
            default:
                rsc->setError(RS_ERROR_DRIVER, buf);
                break;
            }
        }

        ALOGE("%p, %s", rsc, buf);
    }

}

void rsdGLClearColor(const android::renderscript::Context *rsc,
                     float r, float g, float b, float a) {
    RSD_CALL_GL(glClearColor, r, g, b, a);
    RSD_CALL_GL(glClear, GL_COLOR_BUFFER_BIT);
}

void rsdGLClearDepth(const android::renderscript::Context *rsc, float v) {
    RSD_CALL_GL(glClearDepthf, v);
    RSD_CALL_GL(glClear, GL_DEPTH_BUFFER_BIT);
}

void rsdGLFinish(const android::renderscript::Context *rsc) {
    RSD_CALL_GL(glFinish);
}

void rsdGLDrawQuadTexCoords(const android::renderscript::Context *rsc,
                            float x1, float y1, float z1, float u1, float v1,
                            float x2, float y2, float z2, float u2, float v2,
                            float x3, float y3, float z3, float u3, float v3,
                            float x4, float y4, float z4, float u4, float v4) {

    float vtx[] = {x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4};
    const float tex[] = {u1,v1, u2,v2, u3,v3, u4,v4};

    RsdVertexArray::Attrib attribs[2];

    attribs[0].set(GL_FLOAT, 3, 12, false, (size_t)vtx, "ATTRIB_position");
    attribs[1].set(GL_FLOAT, 2, 8, false, (size_t)tex, "ATTRIB_texture0");

    RsdVertexArray va(attribs, 2);
    va.setup(rsc);

    RSD_CALL_GL(glDrawArrays, GL_TRIANGLE_FAN, 0, 4);
}