/****************************************************************************
 Copyright (c) 2019-2023 Xiamen Yaji Software Co., Ltd.

 http://www.cocos.com

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights to
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 of the Software, and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
****************************************************************************/

#include "GLES2Std.h"

#include "GLES2Commands.h"
#include "GLES2Device.h"
#include "gfx-gles-common/GLESCommandPool.h"

#define BUFFER_OFFSET(idx) (static_cast<char *>(0) + (idx))

namespace cc {
namespace gfx {

namespace {

GLenum mapGLFormat(Format format) {
    switch (format) {
        case Format::A8: return GL_ALPHA;
        case Format::L8: return GL_LUMINANCE;
        case Format::LA8: return GL_LUMINANCE_ALPHA;

        case Format::R8:
        case Format::R8SN:
        case Format::R16F:
        case Format::R32F: return GL_RED_EXT;
        case Format::RG8:
        case Format::RG8SN:
        case Format::RG16F:
        case Format::RG32F: return GL_RG_EXT;
        case Format::RGB8:
        case Format::RGB8SN:
        case Format::RGB16F:
        case Format::RGB32F:
        case Format::R5G6B5:
        case Format::SRGB8: return GL_RGB;
        case Format::RGBA8:
        case Format::RGBA8SN:
        case Format::RGBA16F:
        case Format::RGBA32F:
        case Format::RGBA4:
        case Format::RGB5A1:
        case Format::RGB10A2:
        case Format::SRGB8_A8: return GL_RGBA;

        case Format::DEPTH: return GL_DEPTH_COMPONENT;
        case Format::DEPTH_STENCIL: return GL_DEPTH_STENCIL_OES;

        case Format::BC1: return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
        case Format::BC1_ALPHA: return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
        case Format::BC1_SRGB: return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
        case Format::BC1_SRGB_ALPHA: return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
        case Format::BC2: return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
        case Format::BC2_SRGB: return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
        case Format::BC3: return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        case Format::BC3_SRGB: return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;

        case Format::ETC_RGB8: return GL_ETC1_RGB8_OES;

        case Format::PVRTC_RGB2: return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
        case Format::PVRTC_RGBA2: return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
        case Format::PVRTC_RGB4: return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
        case Format::PVRTC_RGBA4: return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;

        case Format::ASTC_RGBA_4X4: return GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
        case Format::ASTC_RGBA_5X4: return GL_COMPRESSED_RGBA_ASTC_5x4_KHR;
        case Format::ASTC_RGBA_5X5: return GL_COMPRESSED_RGBA_ASTC_5x5_KHR;
        case Format::ASTC_RGBA_6X5: return GL_COMPRESSED_RGBA_ASTC_6x5_KHR;
        case Format::ASTC_RGBA_6X6: return GL_COMPRESSED_RGBA_ASTC_6x6_KHR;
        case Format::ASTC_RGBA_8X5: return GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
        case Format::ASTC_RGBA_8X6: return GL_COMPRESSED_RGBA_ASTC_8x6_KHR;
        case Format::ASTC_RGBA_8X8: return GL_COMPRESSED_RGBA_ASTC_8x8_KHR;
        case Format::ASTC_RGBA_10X5: return GL_COMPRESSED_RGBA_ASTC_10x5_KHR;
        case Format::ASTC_RGBA_10X6: return GL_COMPRESSED_RGBA_ASTC_10x6_KHR;
        case Format::ASTC_RGBA_10X8: return GL_COMPRESSED_RGBA_ASTC_10x8_KHR;
        case Format::ASTC_RGBA_10X10: return GL_COMPRESSED_RGBA_ASTC_10x10_KHR;
        case Format::ASTC_RGBA_12X10: return GL_COMPRESSED_RGBA_ASTC_12x10_KHR;
        case Format::ASTC_RGBA_12X12: return GL_COMPRESSED_RGBA_ASTC_12x12_KHR;

        case Format::ASTC_SRGBA_4X4: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR;
        case Format::ASTC_SRGBA_5X4: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR;
        case Format::ASTC_SRGBA_5X5: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR;
        case Format::ASTC_SRGBA_6X5: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR;
        case Format::ASTC_SRGBA_6X6: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR;
        case Format::ASTC_SRGBA_8X5: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR;
        case Format::ASTC_SRGBA_8X6: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR;
        case Format::ASTC_SRGBA_8X8: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR;
        case Format::ASTC_SRGBA_10X5: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR;
        case Format::ASTC_SRGBA_10X6: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR;
        case Format::ASTC_SRGBA_10X8: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR;
        case Format::ASTC_SRGBA_10X10: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR;
        case Format::ASTC_SRGBA_12X10: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR;
        case Format::ASTC_SRGBA_12X12: return GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR;

        default: {
            CC_ABORT();
            return GL_NONE;
        }
    }
}

GLenum mapGLInternalFormat(Format format) {
    switch (format) {
        case Format::R8: return GL_R8_EXT;
        case Format::RG8: return GL_RG8_EXT;
        case Format::SRGB8: return GL_SRGB_EXT;
        case Format::SRGB8_A8: return GL_SRGB_ALPHA_EXT;
        case Format::R16F: return GL_R16F_EXT;
        case Format::RG16F: return GL_RG16F_EXT;
        case Format::RGB16F: return GL_RGB16F_EXT;
        case Format::RGBA16F: return GL_RGBA16F_EXT;
        case Format::R32F: return GL_R32F_EXT;
        case Format::RG32F: return GL_RG32F_EXT;
        case Format::RGB32F: return GL_RGB32F_EXT;
        case Format::RGBA32F: return GL_RGBA32F_EXT;
        case Format::R5G6B5: return GL_RGB565;
        case Format::RGB5A1: return GL_RGB5_A1;
        case Format::RGBA4: return GL_RGBA4;
        case Format::DEPTH: return GL_DEPTH_COMPONENT16;
        case Format::DEPTH_STENCIL: return GL_DEPTH24_STENCIL8_OES;

        default: return mapGLFormat(format);
    }
}

GLenum mapGLType(Type type) {
    switch (type) {
        case Type::BOOL: return GL_BOOL;
        case Type::BOOL2: return GL_BOOL_VEC2;
        case Type::BOOL3: return GL_BOOL_VEC3;
        case Type::BOOL4: return GL_BOOL_VEC4;
        case Type::INT: return GL_INT;
        case Type::INT2: return GL_INT_VEC2;
        case Type::INT3: return GL_INT_VEC3;
        case Type::INT4: return GL_INT_VEC4;
        case Type::UINT: return GL_UNSIGNED_INT;
        case Type::FLOAT: return GL_FLOAT;
        case Type::FLOAT2: return GL_FLOAT_VEC2;
        case Type::FLOAT3: return GL_FLOAT_VEC3;
        case Type::FLOAT4: return GL_FLOAT_VEC4;
        case Type::MAT2: return GL_FLOAT_MAT2;
        case Type::MAT3: return GL_FLOAT_MAT3;
        case Type::MAT4: return GL_FLOAT_MAT4;
        case Type::SAMPLER2D: return GL_SAMPLER_2D;
        case Type::SAMPLER3D: return GL_SAMPLER_3D_OES;
        case Type::SAMPLER_CUBE: return GL_SAMPLER_CUBE;
        default: {
            CC_ABORT();
            return GL_NONE;
        }
    }
}

Type mapType(GLenum glType) {
    switch (glType) {
        case GL_BOOL: return Type::BOOL;
        case GL_BOOL_VEC2: return Type::BOOL2;
        case GL_BOOL_VEC3: return Type::BOOL3;
        case GL_BOOL_VEC4: return Type::BOOL4;
        case GL_INT: return Type::INT;
        case GL_INT_VEC2: return Type::INT2;
        case GL_INT_VEC3: return Type::INT3;
        case GL_INT_VEC4: return Type::INT4;
        case GL_UNSIGNED_INT: return Type::UINT;
        case GL_FLOAT: return Type::FLOAT;
        case GL_FLOAT_VEC2: return Type::FLOAT2;
        case GL_FLOAT_VEC3: return Type::FLOAT3;
        case GL_FLOAT_VEC4: return Type::FLOAT4;
        case GL_FLOAT_MAT2: return Type::MAT2;
        case GL_FLOAT_MAT3: return Type::MAT3;
        case GL_FLOAT_MAT4: return Type::MAT4;
        case GL_SAMPLER_2D: return Type::SAMPLER2D;
        case GL_SAMPLER_3D_OES: return Type::SAMPLER3D;
        case GL_SAMPLER_CUBE: return Type::SAMPLER_CUBE;
        default: {
            CC_ABORT();
            return Type::UNKNOWN;
        }
    }
}

GLenum formatToGLType(Format format) {
    switch (format) {
        case Format::R8: return GL_UNSIGNED_BYTE;
        case Format::R8SN: return GL_BYTE;
        case Format::R8UI: return GL_UNSIGNED_BYTE;
        case Format::R8I: return GL_BYTE;
        case Format::R16F: return GL_HALF_FLOAT_OES;
        case Format::R16UI: return GL_UNSIGNED_SHORT;
        case Format::R16I: return GL_SHORT;
        case Format::R32F: return GL_FLOAT;
        case Format::R32UI: return GL_UNSIGNED_INT;
        case Format::R32I: return GL_INT;

        case Format::RG8: return GL_UNSIGNED_BYTE;
        case Format::RG8SN: return GL_BYTE;
        case Format::RG8UI: return GL_UNSIGNED_BYTE;
        case Format::RG8I: return GL_BYTE;
        case Format::RG16F: return GL_HALF_FLOAT_OES;
        case Format::RG16UI: return GL_UNSIGNED_SHORT;
        case Format::RG16I: return GL_SHORT;
        case Format::RG32F: return GL_FLOAT;
        case Format::RG32UI: return GL_UNSIGNED_INT;
        case Format::RG32I: return GL_INT;

        case Format::RGB8:
        case Format::SRGB8: return GL_UNSIGNED_BYTE;
        case Format::RGB8SN: return GL_BYTE;
        case Format::RGB8UI: return GL_UNSIGNED_BYTE;
        case Format::RGB8I: return GL_BYTE;
        case Format::RGB16F: return GL_HALF_FLOAT_OES;
        case Format::RGB16UI: return GL_UNSIGNED_SHORT;
        case Format::RGB16I: return GL_SHORT;
        case Format::RGB32F: return GL_FLOAT;
        case Format::RGB32UI: return GL_UNSIGNED_INT;
        case Format::RGB32I: return GL_INT;

        case Format::RGBA8:
        case Format::SRGB8_A8: return GL_UNSIGNED_BYTE;
        case Format::RGBA8SN: return GL_BYTE;
        case Format::RGBA8UI: return GL_UNSIGNED_BYTE;
        case Format::RGBA8I: return GL_BYTE;
        case Format::RGBA16F: return GL_HALF_FLOAT_OES;
        case Format::RGBA16UI: return GL_UNSIGNED_SHORT;
        case Format::RGBA16I: return GL_SHORT;
        case Format::RGBA32F: return GL_FLOAT;
        case Format::RGBA32UI: return GL_UNSIGNED_INT;
        case Format::RGBA32I: return GL_INT;

        case Format::R5G6B5: return GL_UNSIGNED_SHORT_5_6_5;
        case Format::RGB5A1: return GL_UNSIGNED_SHORT_5_5_5_1;
        case Format::RGBA4: return GL_UNSIGNED_SHORT_4_4_4_4;
        case Format::RGB9E5: return GL_FLOAT;

        case Format::DEPTH: return GL_UNSIGNED_SHORT;
        case Format::DEPTH_STENCIL: return GL_UNSIGNED_INT_24_8_OES;

        case Format::BC1:
        case Format::BC1_SRGB:
        case Format::BC2:
        case Format::BC2_SRGB:
        case Format::BC3:
        case Format::BC3_SRGB:
        case Format::BC4: return GL_UNSIGNED_BYTE;
        case Format::BC4_SNORM: return GL_BYTE;
        case Format::BC5: return GL_UNSIGNED_BYTE;
        case Format::BC5_SNORM: return GL_BYTE;
        case Format::BC6H_SF16:
        case Format::BC6H_UF16: return GL_FLOAT;
        case Format::BC7:
        case Format::BC7_SRGB:

        case Format::ETC_RGB8:
        case Format::EAC_R11: return GL_UNSIGNED_BYTE;
        case Format::EAC_R11SN: return GL_BYTE;
        case Format::EAC_RG11: return GL_UNSIGNED_BYTE;
        case Format::EAC_RG11SN: return GL_BYTE;

        case Format::PVRTC_RGB2:
        case Format::PVRTC_RGBA2:
        case Format::PVRTC_RGB4:
        case Format::PVRTC_RGBA4:
        case Format::PVRTC2_2BPP:
        case Format::PVRTC2_4BPP:

        case Format::ASTC_RGBA_4X4:
        case Format::ASTC_RGBA_5X4:
        case Format::ASTC_RGBA_5X5:
        case Format::ASTC_RGBA_6X5:
        case Format::ASTC_RGBA_6X6:
        case Format::ASTC_RGBA_8X5:
        case Format::ASTC_RGBA_8X6:
        case Format::ASTC_RGBA_8X8:
        case Format::ASTC_RGBA_10X5:
        case Format::ASTC_RGBA_10X6:
        case Format::ASTC_RGBA_10X8:
        case Format::ASTC_RGBA_10X10:
        case Format::ASTC_RGBA_12X10:
        case Format::ASTC_RGBA_12X12:
        case Format::ASTC_SRGBA_4X4:
        case Format::ASTC_SRGBA_5X4:
        case Format::ASTC_SRGBA_5X5:
        case Format::ASTC_SRGBA_6X5:
        case Format::ASTC_SRGBA_6X6:
        case Format::ASTC_SRGBA_8X5:
        case Format::ASTC_SRGBA_8X6:
        case Format::ASTC_SRGBA_8X8:
        case Format::ASTC_SRGBA_10X5:
        case Format::ASTC_SRGBA_10X6:
        case Format::ASTC_SRGBA_10X8:
        case Format::ASTC_SRGBA_10X10:
        case Format::ASTC_SRGBA_12X10:
        case Format::ASTC_SRGBA_12X12:
            return GL_UNSIGNED_BYTE;

        default: {
            CC_ABORT();
            return GL_NONE;
        }
    }
}

uint32_t glTypeSize(GLenum glType) {
    switch (glType) {
        case GL_BOOL: return 4;
        case GL_BOOL_VEC2: return 8;
        case GL_BOOL_VEC3: return 12;
        case GL_BOOL_VEC4: return 16;
        case GL_INT: return 4;
        case GL_INT_VEC2: return 8;
        case GL_INT_VEC3: return 12;
        case GL_INT_VEC4: return 16;
        case GL_UNSIGNED_INT:
        case GL_FLOAT: return 4;
        case GL_FLOAT_VEC2: return 8;
        case GL_FLOAT_VEC3: return 12;
        case GL_FLOAT_VEC4:
        case GL_FLOAT_MAT2: return 16;
        case GL_FLOAT_MAT3: return 36;
        case GL_FLOAT_MAT4: return 64;
        case GL_SAMPLER_2D:
        case GL_SAMPLER_3D_OES:
        case GL_SAMPLER_CUBE:
        case GL_SAMPLER_CUBE_MAP_ARRAY_OES:
        case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW_OES:
        case GL_INT_SAMPLER_CUBE_MAP_ARRAY_OES:
        case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY_OES: return 4;
        default: {
            CC_ABORT();
            return 0;
        }
    }
}

uint32_t glComponentCount(GLenum glType) {
    switch (glType) {
        case GL_FLOAT_MAT2: return 2;
        case GL_FLOAT_MAT3: return 3;
        case GL_FLOAT_MAT4: return 4;
        default: {
            return 1;
        }
    }
}

const GLenum GLES2_WRAPS[] = {
    GL_REPEAT,
    GL_MIRRORED_REPEAT,
    GL_CLAMP_TO_EDGE,
    GL_CLAMP_TO_EDGE,
};

const GLenum GLES2_CMP_FUNCS[] = {
    GL_NEVER,
    GL_LESS,
    GL_EQUAL,
    GL_LEQUAL,
    GL_GREATER,
    GL_NOTEQUAL,
    GL_GEQUAL,
    GL_ALWAYS,
};

const GLenum GLES2_STENCIL_OPS[] = {
    GL_ZERO,
    GL_KEEP,
    GL_REPLACE,
    GL_INCR,
    GL_DECR,
    GL_INVERT,
    GL_INCR_WRAP,
    GL_DECR_WRAP,
};

const GLenum GLES2_BLEND_OPS[] = {
    GL_FUNC_ADD,
    GL_FUNC_SUBTRACT,
    GL_FUNC_REVERSE_SUBTRACT,
    GL_MIN_EXT,
    GL_MAX_EXT,
};

const GLenum GLES2_BLEND_FACTORS[] = {
    GL_ZERO,
    GL_ONE,
    GL_SRC_ALPHA,
    GL_DST_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA,
    GL_ONE_MINUS_DST_ALPHA,
    GL_SRC_COLOR,
    GL_DST_COLOR,
    GL_ONE_MINUS_SRC_COLOR,
    GL_ONE_MINUS_DST_COLOR,
    GL_SRC_ALPHA_SATURATE,
    GL_CONSTANT_COLOR,
    GL_ONE_MINUS_CONSTANT_COLOR,
    GL_CONSTANT_ALPHA,
    GL_ONE_MINUS_CONSTANT_ALPHA,
};
} // namespace

void cmdFuncGLES2CreateBuffer(GLES2Device *device, GLES2GPUBuffer *gpuBuffer) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    GLenum glUsage = (hasFlag(gpuBuffer->memUsage, MemoryUsageBit::HOST) ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);

    if (hasFlag(gpuBuffer->usage, BufferUsageBit::VERTEX)) {
        gpuBuffer->glTarget = GL_ARRAY_BUFFER;
        GL_CHECK(glGenBuffers(1, &gpuBuffer->glBuffer));
        if (gpuBuffer->size) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;

            if (device->stateCache()->glArrayBuffer != gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, gpuBuffer->glBuffer));
            }

            GL_CHECK(glBufferData(GL_ARRAY_BUFFER, gpuBuffer->size, nullptr, glUsage));
            GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
            device->stateCache()->glArrayBuffer = 0;
        }
    } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDEX)) {
        gpuBuffer->glTarget = GL_ELEMENT_ARRAY_BUFFER;
        GL_CHECK(glGenBuffers(1, &gpuBuffer->glBuffer));
        if (gpuBuffer->size) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;

            if (device->stateCache()->glElementArrayBuffer != gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuBuffer->glBuffer));
            }

            GL_CHECK(glBufferData(GL_ELEMENT_ARRAY_BUFFER, gpuBuffer->size, nullptr, glUsage));
            GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
            device->stateCache()->glElementArrayBuffer = 0;
        }
    } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDIRECT)) {
        gpuBuffer->glTarget = GL_NONE;
    } else if ((hasFlag(gpuBuffer->usage, BufferUsageBit::UNIFORM)) ||
               (hasFlag(gpuBuffer->usage, BufferUsageBit::TRANSFER_DST)) ||
               (hasFlag(gpuBuffer->usage, BufferUsageBit::TRANSFER_SRC))) {
        gpuBuffer->buffer = static_cast<uint8_t *>(CC_MALLOC(gpuBuffer->size));
        gpuBuffer->glTarget = GL_NONE;
    } else {
        CC_ABORT();
        gpuBuffer->glTarget = GL_NONE;
    }
}

void cmdFuncGLES2DestroyBuffer(GLES2Device *device, GLES2GPUBuffer *gpuBuffer) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    if (gpuBuffer->glBuffer) {
        if (hasFlag(gpuBuffer->usage, BufferUsageBit::VERTEX)) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;
            if (device->stateCache()->glArrayBuffer == gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
                device->stateCache()->glArrayBuffer = 0;
            }
        } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDEX)) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;
            if (device->stateCache()->glElementArrayBuffer == gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
                device->stateCache()->glElementArrayBuffer = 0;
            }
        }
        GL_CHECK(glDeleteBuffers(1, &gpuBuffer->glBuffer));
        gpuBuffer->glBuffer = 0;
    }
    CC_SAFE_FREE(gpuBuffer->buffer);
}

void cmdFuncGLES2ResizeBuffer(GLES2Device *device, GLES2GPUBuffer *gpuBuffer) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    GLenum glUsage = (hasFlag(gpuBuffer->memUsage, MemoryUsageBit::HOST) ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);

    if (hasFlag(gpuBuffer->usage, BufferUsageBit::VERTEX)) {
        gpuBuffer->glTarget = GL_ARRAY_BUFFER;
        if (gpuBuffer->size) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;

            if (device->stateCache()->glArrayBuffer != gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, gpuBuffer->glBuffer));
            }

            GL_CHECK(glBufferData(GL_ARRAY_BUFFER, gpuBuffer->size, nullptr, glUsage));
            GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
            device->stateCache()->glArrayBuffer = 0;
        }
    } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDEX)) {
        gpuBuffer->glTarget = GL_ELEMENT_ARRAY_BUFFER;
        if (gpuBuffer->size) {
            if (device->constantRegistry()->useVAO) {
                if (device->stateCache()->glVAO) {
                    GL_CHECK(glBindVertexArrayOES(0));
                    device->stateCache()->glVAO = 0;
                }
            }
            gfxStateCache.gpuInputAssembler = nullptr;

            if (device->stateCache()->glElementArrayBuffer != gpuBuffer->glBuffer) {
                GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuBuffer->glBuffer));
            }

            GL_CHECK(glBufferData(GL_ELEMENT_ARRAY_BUFFER, gpuBuffer->size, nullptr, glUsage));
            GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
            device->stateCache()->glElementArrayBuffer = 0;
        }
    } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDIRECT)) {
        gpuBuffer->indirects.resize(gpuBuffer->count);
        gpuBuffer->glTarget = GL_NONE;
    } else if ((hasFlag(gpuBuffer->usage, BufferUsageBit::UNIFORM)) ||
               (hasFlag(gpuBuffer->usage, BufferUsageBit::TRANSFER_DST)) ||
               (hasFlag(gpuBuffer->usage, BufferUsageBit::TRANSFER_SRC))) {
        if (gpuBuffer->buffer) {
            CC_FREE(gpuBuffer->buffer);
        }
        gpuBuffer->buffer = static_cast<uint8_t *>(CC_MALLOC(gpuBuffer->size));
        gpuBuffer->glTarget = GL_NONE;
    } else {
        CC_ABORT();
        gpuBuffer->glTarget = GL_NONE;
    }
}

void cmdFuncGLES2CreateTexture(GLES2Device *device, GLES2GPUTexture *gpuTexture) {
    gpuTexture->glFormat = mapGLFormat(gpuTexture->format);
    gpuTexture->glType = formatToGLType(gpuTexture->format);
    gpuTexture->glInternalFmt = mapGLInternalFormat(gpuTexture->format);
    gpuTexture->glSamples = static_cast<GLint>(gpuTexture->samples);

    if (gpuTexture->samples > SampleCount::X1) {
        if (device->constantRegistry()->mMSRT != MSRTSupportLevel::NONE &&
            hasFlag(gpuTexture->flags, TextureFlagBit::LAZILY_ALLOCATED)) {
            gpuTexture->memoryAllocated = false;
            return;
        }
    }

    if (gpuTexture->glTexture) {
        gpuTexture->glTarget = GL_TEXTURE_EXTERNAL_OES;
        return;
    }

    if (!device->isTextureExclusive(gpuTexture->format) && (gpuTexture->glSamples > 1 || hasAllFlags(TextureUsage::COLOR_ATTACHMENT | TextureUsage::DEPTH_STENCIL_ATTACHMENT, gpuTexture->usage))) {
        gpuTexture->glInternalFmt = mapGLInternalFormat(gpuTexture->format);
        switch (gpuTexture->type) {
            case TextureType::TEX2D: {
                gpuTexture->glTarget = GL_RENDERBUFFER;
                GL_CHECK(glGenRenderbuffers(1, &gpuTexture->glRenderbuffer));
                if (gpuTexture->size > 0) {
                    GLuint &glRenderbuffer = device->stateCache()->glRenderbuffer;
                    if (gpuTexture->glRenderbuffer != glRenderbuffer) {
                        GL_CHECK(glBindRenderbuffer(GL_RENDERBUFFER, gpuTexture->glRenderbuffer));
                        glRenderbuffer = gpuTexture->glRenderbuffer;
                    }
                    if (gpuTexture->glSamples > 1) {
                        GL_CHECK(glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, gpuTexture->glSamples, gpuTexture->glInternalFmt, gpuTexture->width, gpuTexture->height));
                    } else {
                        GL_CHECK(glRenderbufferStorage(GL_RENDERBUFFER, gpuTexture->glInternalFmt, gpuTexture->width, gpuTexture->height));
                    }
                }
                break;
            }
            default:
                CC_ABORT();
                break;
        }
    } else {
#if CC_PLATFORM == CC_PLATFORM_WINDOWS // PVRVFrame issue
        gpuTexture->glInternalFmt = mapGLInternalFormat(gpuTexture->format);
#endif
        switch (gpuTexture->type) {
            case TextureType::TEX2D: {
                gpuTexture->glTarget = GL_TEXTURE_2D;
                GL_CHECK(glGenTextures(1, &gpuTexture->glTexture));
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_2D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    uint32_t w = gpuTexture->width;
                    uint32_t h = gpuTexture->height;
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage2D(GL_TEXTURE_2D, i, gpuTexture->glInternalFmt, w, h,
                                                  0, gpuTexture->glFormat, gpuTexture->glType,
                                                  nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, 1);
                            GL_CHECK(glCompressedTexImage2D(GL_TEXTURE_2D, i,
                                                            gpuTexture->glInternalFmt, w, h, 0,
                                                            imgSize, nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::TEX2D_ARRAY: {
                gpuTexture->glTarget = GL_TEXTURE_3D;
                CC_ASSERT((std::max(std::max(gpuTexture->width, gpuTexture->height), gpuTexture->arrayLayer) <= device->getCapabilities().max3DTextureSize) && "cmdFuncGLES2CreateTexture: texture2DArray's dimension is too large");
                GL_CHECK(glGenTextures(1, &gpuTexture->glTexture));
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_3D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->arrayLayer;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage3DOES(GL_TEXTURE_3D, i,
                                                     gpuTexture->glInternalFmt,
                                                     w, h, d, 0,
                                                     gpuTexture->glFormat, gpuTexture->glType,
                                                     nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->arrayLayer;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, d);
                            GL_CHECK(glCompressedTexImage3DOES(GL_TEXTURE_3D, i,
                                                               gpuTexture->glInternalFmt,
                                                               w, h, d, 0, imgSize,
                                                               nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::TEX3D: {
                gpuTexture->glTarget = GL_TEXTURE_3D;
                GL_CHECK(glGenTextures(1, &gpuTexture->glTexture));
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_3D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->depth;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage3DOES(GL_TEXTURE_3D, i,
                                                     gpuTexture->glInternalFmt,
                                                     w, h, d, 0,
                                                     gpuTexture->glFormat, gpuTexture->glType,
                                                     nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->depth;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, d);
                            GL_CHECK(glCompressedTexImage3DOES(GL_TEXTURE_3D, i,
                                                               gpuTexture->glInternalFmt,
                                                               w, h, d, 0, imgSize,
                                                               nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::CUBE: {
                gpuTexture->glTarget = GL_TEXTURE_CUBE_MAP;
                GL_CHECK(glGenTextures(1, &gpuTexture->glTexture));
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_CUBE_MAP, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        for (uint32_t f = 0; f < 6; ++f) {
                            uint32_t w = gpuTexture->width;
                            uint32_t h = gpuTexture->height;
                            for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                                GL_CHECK(glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f, i,
                                                      gpuTexture->glInternalFmt, w, h, 0,
                                                      gpuTexture->glFormat, gpuTexture->glType,
                                                      nullptr));
                                w = std::max(1U, w >> 1);
                                h = std::max(1U, h >> 1);
                            }
                        }
                    } else {
                        for (uint32_t f = 0; f < 6; ++f) {
                            uint32_t w = gpuTexture->width;
                            uint32_t h = gpuTexture->height;
                            for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                                uint32_t imgSize = formatSize(gpuTexture->format, w, h, 1);
                                GL_CHECK(glCompressedTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f,
                                                                i, gpuTexture->glInternalFmt, w, h,
                                                                0, imgSize, nullptr));
                                w = std::max(1U, w >> 1);
                                h = std::max(1U, h >> 1);
                            }
                        }
                    }
                }
                break;
            }
            default:
                CC_ABORT();
                break;
        }
    }
}

void cmdFuncGLES2DestroyTexture(GLES2Device *device, GLES2GPUTexture *gpuTexture) {
    device->framebufferCacheMap()->onTextureDestroy(gpuTexture);

    if (gpuTexture->glTexture) {
        for (GLuint &glTexture : device->stateCache()->glTextures) {
            if (glTexture == gpuTexture->glTexture) {
                glTexture = 0;
            }
        }
        if (gpuTexture->glTarget != GL_TEXTURE_EXTERNAL_OES) {
            GL_CHECK(glDeleteTextures(1, &gpuTexture->glTexture));
        }
        gpuTexture->glTexture = 0;

    } else if (gpuTexture->glRenderbuffer) {
        GLuint &glRenderbuffer = device->stateCache()->glRenderbuffer;
        if (gpuTexture->glRenderbuffer == glRenderbuffer) {
            GL_CHECK(glBindRenderbuffer(GL_RENDERBUFFER, 0));
            glRenderbuffer = 0;
        }
        GL_CHECK(glDeleteRenderbuffers(1, &gpuTexture->glRenderbuffer));
        gpuTexture->glRenderbuffer = 0;
    }
}

void cmdFuncGLES2ResizeTexture(GLES2Device *device, GLES2GPUTexture *gpuTexture) {
    if (!gpuTexture->memoryAllocated || gpuTexture->glTarget == GL_TEXTURE_EXTERNAL_OES) return;

    if (gpuTexture->glSamples <= 1) {
        switch (gpuTexture->type) {
            case TextureType::TEX2D: {
                gpuTexture->glTarget = GL_TEXTURE_2D;
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_2D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    uint32_t w = gpuTexture->width;
                    uint32_t h = gpuTexture->height;
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage2D(GL_TEXTURE_2D, i, gpuTexture->glInternalFmt, w, h, 0, gpuTexture->glFormat, gpuTexture->glType, nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, 1);
                            GL_CHECK(glCompressedTexImage2D(GL_TEXTURE_2D, i, gpuTexture->glInternalFmt, w, h, 0, imgSize, nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::TEX2D_ARRAY: {
                gpuTexture->glTarget = GL_TEXTURE_2D_ARRAY;
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_3D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->arrayLayer;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage3DOES(GL_TEXTURE_3D, i,
                                                     gpuTexture->glInternalFmt,
                                                     w, h, d, 0,
                                                     gpuTexture->glFormat, gpuTexture->glType,
                                                     nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->arrayLayer;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, d);
                            GL_CHECK(glCompressedTexImage3DOES(GL_TEXTURE_3D, i,
                                                               gpuTexture->glInternalFmt,
                                                               w, h, d, 0, imgSize,
                                                               nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::TEX3D: {
                gpuTexture->glTarget = GL_TEXTURE_3D;
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_3D, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->depth;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            GL_CHECK(glTexImage3DOES(GL_TEXTURE_3D, i,
                                                     gpuTexture->glInternalFmt,
                                                     w, h, d, 0,
                                                     gpuTexture->glFormat, gpuTexture->glType,
                                                     nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    } else {
                        uint32_t w = gpuTexture->width;
                        uint32_t h = gpuTexture->height;
                        uint32_t d = gpuTexture->depth;
                        for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                            uint32_t imgSize = formatSize(gpuTexture->format, w, h, d);
                            GL_CHECK(glCompressedTexImage3DOES(GL_TEXTURE_3D, i,
                                                               gpuTexture->glInternalFmt,
                                                               w, h, d, 0, imgSize,
                                                               nullptr));
                            w = std::max(1U, w >> 1);
                            h = std::max(1U, h >> 1);
                        }
                    }
                }
                break;
            }
            case TextureType::CUBE: {
                gpuTexture->glTarget = GL_TEXTURE_CUBE_MAP;
                if (gpuTexture->size > 0) {
                    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
                    if (gpuTexture->glTexture != glTexture) {
                        GL_CHECK(glBindTexture(GL_TEXTURE_CUBE_MAP, gpuTexture->glTexture));
                        glTexture = gpuTexture->glTexture;
                    }
                    if (!GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed) {
                        for (uint32_t f = 0; f < 6; ++f) {
                            uint32_t w = gpuTexture->width;
                            uint32_t h = gpuTexture->height;
                            for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                                GL_CHECK(glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f, i, gpuTexture->glInternalFmt, w, h, 0, gpuTexture->glFormat, gpuTexture->glType, nullptr));
                                w = std::max(1U, w >> 1);
                                h = std::max(1U, h >> 1);
                            }
                        }
                    } else {
                        for (uint32_t f = 0; f < 6; ++f) {
                            uint32_t w = gpuTexture->width;
                            uint32_t h = gpuTexture->height;
                            for (uint32_t i = 0; i < gpuTexture->mipLevel; ++i) {
                                uint32_t imgSize = formatSize(gpuTexture->format, w, h, 1);
                                GL_CHECK(glCompressedTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f, i, gpuTexture->glInternalFmt, w, h, 0, imgSize, nullptr));
                                w = std::max(1U, w >> 1);
                                h = std::max(1U, h >> 1);
                            }
                        }
                    }
                }
                break;
            }
            default:
                CC_ABORT();
                break;
        }
    } else {
        switch (gpuTexture->type) {
            case TextureType::TEX2D: {
                if (gpuTexture->size > 0) {
                    GLuint &glRenderbuffer = device->stateCache()->glRenderbuffer;
                    if (gpuTexture->glRenderbuffer != glRenderbuffer) {
                        GL_CHECK(glBindRenderbuffer(GL_RENDERBUFFER, gpuTexture->glRenderbuffer));
                        glRenderbuffer = gpuTexture->glRenderbuffer;
                    }
                    if (gpuTexture->glSamples > 1) {
                        GL_CHECK(glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, gpuTexture->glSamples, gpuTexture->glInternalFmt, gpuTexture->width, gpuTexture->height));
                    } else {
                        GL_CHECK(glRenderbufferStorage(GL_RENDERBUFFER, gpuTexture->glInternalFmt, gpuTexture->width, gpuTexture->height));
                    }
                }
                break;
            }
            default:
                CC_ABORT();
                break;
        }
    }
}

void cmdFuncGLES2CreateSampler(GLES2Device * /*device*/, GLES2GPUSampler *gpuSampler) {
    if (gpuSampler->minFilter == Filter::LINEAR || gpuSampler->minFilter == Filter::ANISOTROPIC) {
        if (gpuSampler->mipFilter == Filter::LINEAR || gpuSampler->mipFilter == Filter::ANISOTROPIC) {
            gpuSampler->glMinFilter = GL_LINEAR_MIPMAP_LINEAR;
        } else if (gpuSampler->mipFilter == Filter::POINT) {
            gpuSampler->glMinFilter = GL_LINEAR_MIPMAP_NEAREST;
        } else {
            gpuSampler->glMinFilter = GL_LINEAR;
        }
    } else {
        if (gpuSampler->mipFilter == Filter::LINEAR || gpuSampler->mipFilter == Filter::ANISOTROPIC) {
            gpuSampler->glMinFilter = GL_NEAREST_MIPMAP_LINEAR;
        } else if (gpuSampler->mipFilter == Filter::POINT) {
            gpuSampler->glMinFilter = GL_NEAREST_MIPMAP_NEAREST;
        } else {
            gpuSampler->glMinFilter = GL_NEAREST;
        }
    }

    if (gpuSampler->magFilter == Filter::LINEAR || gpuSampler->magFilter == Filter::ANISOTROPIC) {
        gpuSampler->glMagFilter = GL_LINEAR;
    } else {
        gpuSampler->glMagFilter = GL_NEAREST;
    }

    gpuSampler->glWrapS = GLES2_WRAPS[static_cast<int>(gpuSampler->addressU)];
    gpuSampler->glWrapT = GLES2_WRAPS[static_cast<int>(gpuSampler->addressV)];
    gpuSampler->glWrapR = GLES2_WRAPS[static_cast<int>(gpuSampler->addressW)];
}

void cmdFuncGLES2DestroySampler(GLES2Device *device, GLES2GPUSampler *gpuSampler) {
}

void cmdFuncGLES2CreateShader(GLES2Device *device, GLES2GPUShader *gpuShader) {
    GLenum glShaderType = 0;
    ccstd::string shaderTypeStr;
    GLint status;

    for (size_t i = 0; i < gpuShader->gpuStages.size(); ++i) {
        GLES2GPUShaderStage &gpuStage = gpuShader->gpuStages[i];

        switch (gpuStage.type) {
            case ShaderStageFlagBit::VERTEX: {
                glShaderType = GL_VERTEX_SHADER;
                shaderTypeStr = "Vertex Shader";
                break;
            }
            case ShaderStageFlagBit::FRAGMENT: {
                glShaderType = GL_FRAGMENT_SHADER;
                shaderTypeStr = "Fragment Shader";
                break;
            }
            default: {
                CC_ABORT();
                return;
            }
        }

        GL_CHECK(gpuStage.glShader = glCreateShader(glShaderType));
        const char *shaderSrc = gpuStage.source.c_str();
        GL_CHECK(glShaderSource(gpuStage.glShader, 1, (const GLchar **)&shaderSrc, nullptr));
        GL_CHECK(glCompileShader(gpuStage.glShader));

        GL_CHECK(glGetShaderiv(gpuStage.glShader, GL_COMPILE_STATUS, &status));
        if (status != 1) {
            GLint logSize = 0;
            GL_CHECK(glGetShaderiv(gpuStage.glShader, GL_INFO_LOG_LENGTH, &logSize));

            ++logSize;
            auto *logs = static_cast<GLchar *>(CC_MALLOC(logSize));
            GL_CHECK(glGetShaderInfoLog(gpuStage.glShader, logSize, nullptr, logs));

            CC_LOG_ERROR("%s in %s compilation failed.", shaderTypeStr.c_str(), gpuShader->name.c_str());
            CC_LOG_ERROR(logs);
            CC_FREE(logs);
            GL_CHECK(glDeleteShader(gpuStage.glShader));
            gpuStage.glShader = 0;
            return;
        }
    }

    GL_CHECK(gpuShader->glProgram = glCreateProgram());

    // link program
    for (size_t i = 0; i < gpuShader->gpuStages.size(); ++i) {
        GLES2GPUShaderStage &gpuStage = gpuShader->gpuStages[i];
        GL_CHECK(glAttachShader(gpuShader->glProgram, gpuStage.glShader));
    }

    GL_CHECK(glLinkProgram(gpuShader->glProgram));

    // detach & delete immediately
    for (size_t i = 0; i < gpuShader->gpuStages.size(); ++i) {
        GLES2GPUShaderStage &gpuStage = gpuShader->gpuStages[i];
        if (gpuStage.glShader) {
            GL_CHECK(glDetachShader(gpuShader->glProgram, gpuStage.glShader));
            GL_CHECK(glDeleteShader(gpuStage.glShader));
            gpuStage.glShader = 0;
        }
    }

    GL_CHECK(glGetProgramiv(gpuShader->glProgram, GL_LINK_STATUS, &status));
    if (status != 1) {
        CC_LOG_ERROR("Failed to link Shader [%s].", gpuShader->name.c_str());
        GLint logSize = 0;
        GL_CHECK(glGetProgramiv(gpuShader->glProgram, GL_INFO_LOG_LENGTH, &logSize));
        if (logSize) {
            ++logSize;
            auto *logs = static_cast<GLchar *>(CC_MALLOC(logSize));
            GL_CHECK(glGetProgramInfoLog(gpuShader->glProgram, logSize, nullptr, logs));

            CC_LOG_ERROR(logs);
            CC_FREE(logs);
            return;
        }
    }

    CC_LOG_INFO("Shader '%s' compilation succeeded.", gpuShader->name.c_str());

    GLint attrMaxLength = 0;
    GLint attrCount = 0;
    GL_CHECK(glGetProgramiv(gpuShader->glProgram, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &attrMaxLength));
    GL_CHECK(glGetProgramiv(gpuShader->glProgram, GL_ACTIVE_ATTRIBUTES, &attrCount));

    GLchar glName[256];
    GLsizei glLength;
    GLsizei glSize;
    GLenum glType;

    gpuShader->glInputs.resize(attrCount);
    for (GLint i = 0; i < attrCount; ++i) {
        GLES2GPUInput &gpuInput = gpuShader->glInputs[i];

        memset(glName, 0, sizeof(glName));
        GL_CHECK(glGetActiveAttrib(gpuShader->glProgram, i, attrMaxLength, &glLength, &glSize, &glType, glName));
        char *offset = strchr(glName, '[');
        if (offset) {
            glName[offset - glName] = '\0';
        }

        gpuInput.glLoc = glGetAttribLocation(gpuShader->glProgram, glName);
        gpuInput.binding = gpuInput.glLoc;
        gpuInput.name = glName;
        gpuInput.type = mapType(glType);
        gpuInput.stride = glTypeSize(glType);
        gpuInput.count = glSize;
        gpuInput.size = gpuInput.stride * gpuInput.count;
        gpuInput.glType = glType;
    }

    // create uniform blocks
    if (!gpuShader->blocks.empty()) {
        gpuShader->glBlocks.resize(gpuShader->blocks.size());

        for (size_t i = 0; i < gpuShader->glBlocks.size(); ++i) {
            GLES2GPUUniformBlock &gpuBlock = gpuShader->glBlocks[i];
            UniformBlock &block = gpuShader->blocks[i];

            gpuBlock.name = block.name;
            gpuBlock.set = block.set;
            gpuBlock.binding = block.binding;
            gpuBlock.glUniforms.resize(block.members.size());

            for (size_t j = 0; j < gpuBlock.glUniforms.size(); ++j) {
                GLES2GPUUniform &gpuUniform = gpuBlock.glUniforms[j];
                Uniform &uniform = block.members[j];

                gpuUniform.binding = INVALID_BINDING;
                gpuUniform.name = uniform.name;
                gpuUniform.type = uniform.type;
                gpuUniform.stride = getTypeSize(uniform.type);
                gpuUniform.count = uniform.count;
                gpuUniform.size = gpuUniform.stride * gpuUniform.count;
                gpuUniform.glType = mapGLType(gpuUniform.type);
                gpuUniform.glLoc = -1;
            }
        }
    } // if

    // fallback subpassInputs into samplerTextures if not using FBF
    if (device->constantRegistry()->mFBF == FBFSupportLevel::NONE) {
        for (const auto &subpassInput : gpuShader->subpassInputs) {
            gpuShader->samplerTextures.emplace_back();
            auto &samplerTexture = gpuShader->samplerTextures.back();
            samplerTexture.name = subpassInput.name;
            samplerTexture.set = subpassInput.set;
            samplerTexture.binding = subpassInput.binding;
            samplerTexture.count = subpassInput.count;
            samplerTexture.type = Type::SAMPLER2D;
        }
    }

    // create uniform samplers
    if (!gpuShader->samplerTextures.empty()) {
        gpuShader->glSamplerTextures.resize(gpuShader->samplerTextures.size());

        for (size_t i = 0; i < gpuShader->glSamplerTextures.size(); ++i) {
            UniformSamplerTexture &samplerTexture = gpuShader->samplerTextures[i];
            GLES2GPUUniformSamplerTexture &gpuSamplerTexture = gpuShader->glSamplerTextures[i];
            gpuSamplerTexture.set = samplerTexture.set;
            gpuSamplerTexture.binding = samplerTexture.binding;
            gpuSamplerTexture.name = samplerTexture.name;
            gpuSamplerTexture.count = samplerTexture.count;
            gpuSamplerTexture.glType = mapGLType(samplerTexture.type);
            gpuSamplerTexture.glLoc = -1;
        }
    }

    // parse glUniforms
    GLint glActiveUniformCount;
    GL_CHECK(glGetProgramiv(gpuShader->glProgram, GL_ACTIVE_UNIFORMS, &glActiveUniformCount));

    for (GLint i = 0; i < glActiveUniformCount; ++i) {
        memset(glName, 0, sizeof(glName));
        GL_CHECK(glGetActiveUniform(gpuShader->glProgram, i, 255, &glLength, &glSize, &glType, glName));
        char *offset = strchr(glName, '[');
        if (offset) {
            glName[offset - glName] = '\0';
        }
        ccstd::string name = glName;

        bool isSampler = (glType == GL_SAMPLER_2D) ||
                         (glType == GL_SAMPLER_3D_OES) ||
                         (glType == GL_SAMPLER_CUBE) ||
                         (glType == GL_SAMPLER_CUBE_MAP_ARRAY_OES) ||
                         (glType == GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW_OES) ||
                         (glType == GL_INT_SAMPLER_CUBE_MAP_ARRAY_OES) ||
                         (glType == GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY_OES);
        if (!isSampler) {
            for (auto &glBlock : gpuShader->glBlocks) {
                for (uint32_t j = 0; j < glBlock.glUniforms.size(); ++j) {
                    auto &glUniform = glBlock.glUniforms[j];
                    if (glUniform.name == name) {
                        glUniform.glLoc = glGetUniformLocation(gpuShader->glProgram, glName);
                        glUniform.count = glSize;
                        glUniform.size = glUniform.stride * glUniform.count;

                        glBlock.glActiveUniforms.emplace_back(glUniform);
                        auto &activeUniform = glBlock.glActiveUniforms.back();
                        activeUniform.buff.resize(activeUniform.size);
                        glBlock.activeUniformIndices.push_back(j);
                        break;
                    }
                }
            }
        }
    } // for

    // calculate offset & size
    // WARNING: we can't handle inactive uniform arrays with wrong input sizes
    // and there is no way to detect that for now
    for (auto &gpuBlock : gpuShader->glBlocks) {
        for (auto &gpuUniform : gpuBlock.glUniforms) {
            gpuUniform.offset = gpuBlock.size;
            gpuBlock.size += gpuUniform.size;
        }
        for (uint32_t i = 0; i < gpuBlock.glActiveUniforms.size(); ++i) {
            auto &activeUniform = gpuBlock.glActiveUniforms[i];
            uint32_t index = gpuBlock.activeUniformIndices[i];
            activeUniform.offset = gpuBlock.glUniforms[index].offset;
        }
    }

    // texture unit index mapping optimization
    ccstd::vector<GLES2GPUUniformSamplerTexture> glActiveSamplerTextures;
    ccstd::vector<GLint> glActiveSamplerLocations;
    const GLESBindingMapping &bindingMappings = device->bindingMappings();
    ccstd::unordered_map<ccstd::string, uint32_t> &texUnitCacheMap = device->stateCache()->texUnitCacheMap;

    // sampler bindings in the flexible set comes strictly after buffer bindings
    // so we need to subtract the buffer count for these samplers
    uint32_t flexibleSetBaseOffset = 0U;
    for (const auto &block : gpuShader->blocks) {
        if (block.set == bindingMappings.flexibleSet) {
            flexibleSetBaseOffset++;
        }
    }

    uint32_t arrayOffset = 0U;

    for (uint32_t i = 0U; i < gpuShader->samplerTextures.size(); i++) {
        const UniformSamplerTexture &samplerTexture = gpuShader->samplerTextures[i];
        GLint glLoc = glGetUniformLocation(gpuShader->glProgram, samplerTexture.name.c_str());
        if (glLoc >= 0) {
            glActiveSamplerTextures.push_back(gpuShader->glSamplerTextures[i]);
            glActiveSamplerLocations.push_back(glLoc);
        }
        if (!texUnitCacheMap.count(samplerTexture.name)) {
            uint32_t binding = samplerTexture.binding + bindingMappings.samplerTextureOffsets[samplerTexture.set] + arrayOffset;
            if (samplerTexture.set == bindingMappings.flexibleSet) binding -= flexibleSetBaseOffset;
            texUnitCacheMap[samplerTexture.name] = binding % device->getCapabilities().maxTextureUnits;
            arrayOffset += samplerTexture.count - 1;
        }
    }

    if (!glActiveSamplerTextures.empty()) {
        ccstd::vector<bool> usedTexUnits(device->getCapabilities().maxTextureUnits, false);
        // try to reuse existing mappings first
        for (uint32_t i = 0U; i < glActiveSamplerTextures.size(); i++) {
            GLES2GPUUniformSamplerTexture &glSamplerTexture = glActiveSamplerTextures[i];

            if (texUnitCacheMap.count(glSamplerTexture.name)) {
                uint32_t cachedUnit = texUnitCacheMap[glSamplerTexture.name];
                glSamplerTexture.glLoc = glActiveSamplerLocations[i];
                for (uint32_t t = 0U; t < glSamplerTexture.count; t++) {
                    while (usedTexUnits[cachedUnit]) { // the shader already compiles so we should be safe to do this here
                        cachedUnit = (cachedUnit + 1) % device->getCapabilities().maxTextureUnits;
                    }
                    glSamplerTexture.units.push_back(static_cast<GLint>(cachedUnit));
                    usedTexUnits[cachedUnit] = true;
                }
            }
        }
        // fill in the rest sequencially
        uint32_t unitIdx = 0U;
        for (uint32_t i = 0U; i < glActiveSamplerTextures.size(); i++) {
            GLES2GPUUniformSamplerTexture &glSamplerTexture = glActiveSamplerTextures[i];

            if (glSamplerTexture.glLoc < 0) {
                glSamplerTexture.glLoc = glActiveSamplerLocations[i];
                for (uint32_t t = 0U; t < glSamplerTexture.count; t++) {
                    while (usedTexUnits[unitIdx]) {
                        unitIdx = (unitIdx + 1) % device->getCapabilities().maxTextureUnits;
                    }
                    if (!texUnitCacheMap.count(glSamplerTexture.name)) {
                        texUnitCacheMap[glSamplerTexture.name] = unitIdx;
                    }
                    glSamplerTexture.units.push_back(static_cast<GLint>(unitIdx));
                    usedTexUnits[unitIdx] = true;
                }
            }
        }

        if (device->stateCache()->glProgram != gpuShader->glProgram) {
            GL_CHECK(glUseProgram(gpuShader->glProgram));
        }

        for (auto &gpuSamplerTexture : glActiveSamplerTextures) {
            GL_CHECK(glUniform1iv(gpuSamplerTexture.glLoc, (GLsizei)gpuSamplerTexture.units.size(), gpuSamplerTexture.units.data()));
        }

        if (device->stateCache()->glProgram != gpuShader->glProgram) {
            GL_CHECK(glUseProgram(device->stateCache()->glProgram));
        }
    }

    // strip out the inactive ones
    for (uint32_t i = 0U; i < gpuShader->glBlocks.size();) {
        if (!gpuShader->glBlocks[i].glActiveUniforms.empty()) {
            i++;
        } else {
            gpuShader->glBlocks[i] = gpuShader->glBlocks.back();
            gpuShader->glBlocks.pop_back();
        }
    }
    gpuShader->glSamplerTextures = glActiveSamplerTextures;
}

void cmdFuncGLES2DestroyShader(GLES2Device *device, GLES2GPUShader *gpuShader) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    if (gpuShader->glProgram) {
        if (device->stateCache()->glProgram == gpuShader->glProgram) {
            GL_CHECK(glUseProgram(0));
            device->stateCache()->glProgram = 0;
            gfxStateCache.gpuPipelineState = nullptr;
        }
        GL_CHECK(glDeleteProgram(gpuShader->glProgram));
        gpuShader->glProgram = 0;
    }
}

void cmdFuncGLES2CreateRenderPass(GLES2Device * /*device*/, GLES2GPURenderPass *gpuRenderPass) {
    // calculate the life cycle of each attachments
    auto updateLifeCycle = [](GLES2GPURenderPass::AttachmentStatistics &statistics, uint32_t index) {
        if (statistics.loadSubpass == SUBPASS_EXTERNAL) statistics.loadSubpass = index;
        statistics.storeSubpass = index;
    };
    auto calculateLifeCycle = [&](GLES2GPURenderPass::AttachmentStatistics &statistics, uint32_t targetAttachment) {
        for (uint32_t j = 0U; j < utils::toUint(gpuRenderPass->subpasses.size()); ++j) {
            auto &subpass = gpuRenderPass->subpasses[j];
            for (size_t k = 0U; k < subpass.colors.size(); ++k) {
                if (subpass.colors[k] == targetAttachment) {
                    updateLifeCycle(statistics, j);
                }
                if (!subpass.resolves.empty() && subpass.resolves[k] == targetAttachment) {
                    updateLifeCycle(statistics, j);
                }
            }
            for (unsigned int input : subpass.inputs) {
                if (input == targetAttachment) {
                    updateLifeCycle(statistics, j);
                }
            }
            if (subpass.depthStencil == targetAttachment) {
                updateLifeCycle(statistics, j);
            }
            if (subpass.depthStencilResolve == targetAttachment) {
                updateLifeCycle(statistics, j);
            }
        }
    };

    bool hasDepth = gpuRenderPass->depthStencilAttachment.format != Format::UNKNOWN;
    gpuRenderPass->statistics.resize(gpuRenderPass->colorAttachments.size() + hasDepth);
    for (uint32_t i = 0U; i < utils::toUint(gpuRenderPass->statistics.size()); ++i) {
        calculateLifeCycle(gpuRenderPass->statistics[i], i);
        CC_ASSERT(gpuRenderPass->statistics[i].loadSubpass != SUBPASS_EXTERNAL &&
                  gpuRenderPass->statistics[i].storeSubpass != SUBPASS_EXTERNAL);
    }
}

void cmdFuncGLES2DestroyRenderPass(GLES2Device * /*device*/, GLES2GPURenderPass *gpuRenderPass) {
    gpuRenderPass->statistics.clear();
}

void cmdFuncGLES2CreateInputAssembler(GLES2Device *device, GLES2GPUInputAssembler *gpuInputAssembler) {
    if (gpuInputAssembler->gpuIndexBuffer) {
        switch (gpuInputAssembler->gpuIndexBuffer->stride) {
            case 1: gpuInputAssembler->glIndexType = GL_UNSIGNED_BYTE; break;
            case 2: gpuInputAssembler->glIndexType = GL_UNSIGNED_SHORT; break;
            case 4: gpuInputAssembler->glIndexType = GL_UNSIGNED_INT; break;
            default: {
                CC_LOG_ERROR("Illegal index buffer stride.");
            }
        }
    }

    ccstd::vector<uint32_t> streamOffsets(device->getCapabilities().maxVertexAttributes, 0U);

    gpuInputAssembler->glAttribs.resize(gpuInputAssembler->attributes.size());
    for (size_t i = 0; i < gpuInputAssembler->glAttribs.size(); ++i) {
        GLES2GPUAttribute &gpuAttribute = gpuInputAssembler->glAttribs[i];
        const Attribute &attrib = gpuInputAssembler->attributes[i];

        auto *gpuVB = static_cast<GLES2GPUBuffer *>(gpuInputAssembler->gpuVertexBuffers[attrib.stream]);

        gpuAttribute.name = attrib.name;
        gpuAttribute.glType = formatToGLType(attrib.format);
        gpuAttribute.size = GFX_FORMAT_INFOS[static_cast<int>(attrib.format)].size;
        gpuAttribute.count = GFX_FORMAT_INFOS[static_cast<int>(attrib.format)].count;
        gpuAttribute.componentCount = glComponentCount(gpuAttribute.glType);
        gpuAttribute.isNormalized = attrib.isNormalized;
        gpuAttribute.isInstanced = attrib.isInstanced;
        gpuAttribute.offset = streamOffsets[attrib.stream];

        if (gpuVB) {
            gpuAttribute.glBuffer = gpuVB->glBuffer;
            gpuAttribute.stride = gpuVB->stride;
        }
        streamOffsets[attrib.stream] += gpuAttribute.size;
    }
}

void cmdFuncGLES2DestroyInputAssembler(GLES2Device *device, GLES2GPUInputAssembler *gpuInputAssembler) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    for (auto it = gpuInputAssembler->glVAOs.begin(); it != gpuInputAssembler->glVAOs.end(); ++it) {
        if (device->stateCache()->glVAO == it->second) {
            GL_CHECK(glBindVertexArrayOES(0));
            device->stateCache()->glVAO = 0;
            gfxStateCache.gpuInputAssembler = nullptr;
        }
        GL_CHECK(glDeleteVertexArraysOES(1, &it->second));
    }
    gpuInputAssembler->glVAOs.clear();
}

static GLES2GPUFramebuffer::GLFramebufferInfo doCreateFramebuffer(GLES2Device *device,
                                                                  const ccstd::vector<GLES2GPUTexture *> &attachments, const uint32_t *colors, size_t colorCount,
                                                                  const GLES2GPUTexture *depthStencil,
                                                                  const uint32_t *resolves = nullptr,
                                                                  const GLES2GPUTexture *depthStencilResolve = nullptr,
                                                                  GLbitfield *resolveMask = nullptr) {
    static ccstd::vector<GLenum> drawBuffers;
    GLES2GPUStateCache *cache = device->stateCache();
    GLES2GPUFramebuffer::GLFramebufferInfo res;

    GL_CHECK(glGenFramebuffers(1, &res.glFramebuffer));
    if (cache->glFramebuffer != res.glFramebuffer) {
        GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, res.glFramebuffer));
        cache->glFramebuffer = res.glFramebuffer;
    }

    drawBuffers.clear();

    auto supportLevel = device->constantRegistry()->mMSRT;
    bool autoResolve = supportLevel > MSRTSupportLevel::LEVEL1 || (supportLevel != MSRTSupportLevel::NONE && colorCount <= 1);

    for (size_t j = 0; j < colorCount; ++j) {
        GLES2GPUTexture *gpuColorTexture = attachments[colors[j]];
        GLES2GPUTexture *gpuResolveTexture = resolves ? attachments[resolves[j]] : nullptr;
        drawBuffers.push_back(static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + j));

        if (gpuResolveTexture) {
            if (autoResolve) {
                GL_CHECK(glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + j),
                                                              gpuResolveTexture->glTarget, gpuResolveTexture->glTexture, 0, /* fixed to 0, same for webgl 1 */
                                                              gpuColorTexture->glSamples));
                continue;
            }
            *resolveMask |= GL_COLOR_BUFFER_BIT; // fallback to blit-based manual resolve
        }
        if (gpuColorTexture->glTexture) {
            GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER, static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + j),
                                            gpuColorTexture->glTarget, gpuColorTexture->glTexture, 0));
        } else {
            GL_CHECK(glFramebufferRenderbuffer(GL_FRAMEBUFFER, static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + j),
                                               gpuColorTexture->glTarget, gpuColorTexture->glRenderbuffer));
        }
        res.width = std::min(res.width, gpuColorTexture->width);
        res.height = std::min(res.height, gpuColorTexture->height);
    }
    if (depthStencil) {
        bool hasStencil = GFX_FORMAT_INFOS[static_cast<int>(depthStencil->format)].hasStencil;
        if (depthStencil->glTexture) {
            GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthStencil->glTarget, depthStencil->glTexture, 0));
            if (hasStencil) GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, depthStencil->glTarget, depthStencil->glTexture, 0));
        } else {
            GL_CHECK(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthStencil->glTarget, depthStencil->glRenderbuffer));
            if (hasStencil) GL_CHECK(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, depthStencil->glTarget, depthStencil->glRenderbuffer));
        }

        // fallback to blit-based manual resolve
        if (depthStencilResolve) *resolveMask |= hasStencil ? GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT : GL_DEPTH_BUFFER_BIT;
        res.width = std::min(res.width, depthStencil->width);
        res.height = std::min(res.height, depthStencil->height);
    }

    // register to framebuffer caches
    if (colorCount == 1) device->framebufferCacheMap()->registerExternal(res.glFramebuffer, attachments[colors[0]]);
    if (depthStencil) device->framebufferCacheMap()->registerExternal(res.glFramebuffer, depthStencil);

    if (device->hasFeature(Feature::MULTIPLE_RENDER_TARGETS)) {
        GL_CHECK(glDrawBuffersEXT(utils::toUint(drawBuffers.size()), drawBuffers.data()));
    }

    GLenum status;
    GL_CHECK(status = glCheckFramebufferStatus(GL_FRAMEBUFFER));
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        switch (status) {
            case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                CC_LOG_ERROR("checkFramebufferStatus() - FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
                CC_LOG_ERROR("checkFramebufferStatus() - FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
                CC_LOG_ERROR("checkFramebufferStatus() - FRAMEBUFFER_INCOMPLETE_DIMENSIONS");
                break;
            case GL_FRAMEBUFFER_UNSUPPORTED:
                CC_LOG_ERROR("checkFramebufferStatus() - FRAMEBUFFER_UNSUPPORTED");
                break;
            default:
                CC_LOG_ERROR("checkFramebufferStatus() - %x", status);
                break;
        }
    }

    return res;
}

static GLES2GPUSwapchain *getSwapchainIfExists(const ccstd::vector<GLES2GPUTexture *> &textures, const uint32_t *indices, size_t count) {
    GLES2GPUSwapchain *swapchain{nullptr};
    if (indices) {
        size_t offscreenCount{0};
        for (size_t i = 0; i < count; ++i) {
            auto *colorTexture = textures[indices[i]];
            if (colorTexture->swapchain) {
                swapchain = colorTexture->swapchain;
            } else {
                ++offscreenCount;
            }
        }
        CC_ASSERT(!offscreenCount || offscreenCount == count); // Partially offscreen FBO is not supported.
    }
    return swapchain;
}

static void doCreateFramebufferInstance(GLES2Device *device, GLES2GPUFramebuffer *gpuFBO, const ccstd::vector<uint32_t> &colors,
                                        uint32_t depthStencil, GLES2GPUFramebuffer::Framebuffer *outFBO,
                                        const uint32_t *resolves = nullptr, uint32_t depthStencilResolve = INVALID_BINDING) {
    GLES2GPUSwapchain *swapchain{getSwapchainIfExists(gpuFBO->gpuColorTextures, colors.data(), colors.size())};

    if (!swapchain) {
        const GLES2GPUTexture *depthStencilTexture = nullptr;
        if (depthStencil != INVALID_BINDING) {
            depthStencilTexture = depthStencil < gpuFBO->gpuColorTextures.size()
                                      ? gpuFBO->gpuColorTextures[depthStencil]
                                      : gpuFBO->gpuDepthStencilTexture;
        }
        const GLES2GPUTexture *depthStencilResolveTexture = nullptr;
        if (depthStencilResolve != INVALID_BINDING) {
            depthStencilResolveTexture = depthStencilResolve < gpuFBO->gpuColorTextures.size()
                                             ? gpuFBO->gpuColorTextures[depthStencilResolve]
                                             : gpuFBO->gpuDepthStencilTexture;
        }

        outFBO->framebuffer.initialize(doCreateFramebuffer(device, gpuFBO->gpuColorTextures, colors.data(), colors.size(),
                                                           depthStencilTexture, resolves, depthStencilResolveTexture, &outFBO->resolveMask));
        if (outFBO->resolveMask) {
            size_t resolveCount = outFBO->resolveMask & GL_COLOR_BUFFER_BIT ? colors.size() : 0U;
            GLES2GPUSwapchain *resolveSwapchain{getSwapchainIfExists(gpuFBO->gpuColorTextures, resolves, resolveCount)};
            if (!resolveSwapchain) {
                outFBO->resolveFramebuffer.initialize(doCreateFramebuffer(device, gpuFBO->gpuColorTextures, resolves, resolveCount, depthStencilResolveTexture));
            } else {
                outFBO->resolveFramebuffer.initialize(resolveSwapchain);
            }
        }
    } else {
        outFBO->framebuffer.initialize(swapchain);
    }
}

void cmdFuncGLES2CreateFramebuffer(GLES2Device *device, GLES2GPUFramebuffer *gpuFBO) {
    if (gpuFBO->gpuRenderPass->subpasses.size() > 1) {
        gpuFBO->usesFBF = device->constantRegistry()->mFBF != FBFSupportLevel::NONE;
    }

    if (gpuFBO->usesFBF) {
        for (auto &subpass : gpuFBO->gpuRenderPass->subpasses) {
            if (subpass.inputs.size() == 4) {
                gpuFBO->uberOnChipOutput = subpass.inputs.back();
                gpuFBO->uberFinalOutput = subpass.colors.back();
                break;
            }
        }

        gpuFBO->uberColorAttachmentIndices.clear();
        bool hasDepth{gpuFBO->gpuRenderPass->depthStencilAttachment.format != Format::UNKNOWN};
        gpuFBO->uberDepthStencil = hasDepth ? utils::toUint(gpuFBO->gpuColorTextures.size()) : INVALID_BINDING;
        for (uint32_t i = 0U; i < gpuFBO->gpuColorTextures.size(); ++i) {
            if (i == gpuFBO->uberFinalOutput) continue;
            const auto *gpuTexture = gpuFBO->gpuColorTextures[i];
            if (GFX_FORMAT_INFOS[toNumber(gpuTexture->format)].hasDepth) {
                gpuFBO->uberDepthStencil = i;
                continue;
            }
            gpuFBO->uberColorAttachmentIndices.push_back(i);
        }
        doCreateFramebufferInstance(device, gpuFBO, gpuFBO->uberColorAttachmentIndices, gpuFBO->uberDepthStencil, &gpuFBO->uberInstance);
    } else {
        for (const auto &subpass : gpuFBO->gpuRenderPass->subpasses) {
            gpuFBO->instances.emplace_back();
            auto &fboInst = gpuFBO->instances.back();
            doCreateFramebufferInstance(device, gpuFBO, subpass.colors, subpass.depthStencil, &fboInst,
                                        subpass.resolves.empty() ? nullptr : subpass.resolves.data(), subpass.depthStencilResolve);
        }
    }
}

void GLES2GPUFramebuffer::GLFramebuffer::destroy(GLES2GPUStateCache *cache, GLES2GPUFramebufferCacheMap *framebufferCacheMap) {
    if (swapchain) {
        swapchain = nullptr;
    } else {
        if (cache->glFramebuffer == _glFramebuffer) {
            GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, 0));
            cache->glFramebuffer = 0;
        }
        GL_CHECK(glDeleteFramebuffers(1, &_glFramebuffer));
        framebufferCacheMap->unregisterExternal(_glFramebuffer);
        _glFramebuffer = 0U;
    }
}

void cmdFuncGLES2DestroyFramebuffer(GLES2Device *device, GLES2GPUFramebuffer *gpuFBO) {
    auto *cache = device->stateCache();
    auto *framebufferCacheMap = device->framebufferCacheMap();

    for (auto &instance : gpuFBO->instances) {
        instance.framebuffer.destroy(cache, framebufferCacheMap);
        instance.resolveFramebuffer.destroy(cache, framebufferCacheMap);
    }
    gpuFBO->instances.clear();

    gpuFBO->uberInstance.framebuffer.destroy(cache, framebufferCacheMap);
    gpuFBO->uberInstance.resolveFramebuffer.destroy(cache, framebufferCacheMap);
}

void cmdFuncGLES2BeginRenderPass(GLES2Device *device, uint32_t subpassIdx, GLES2GPURenderPass *gpuRenderPass, GLES2GPUFramebuffer *gpuFramebuffer,
                                 const Rect *renderArea, const Color *clearColors, float clearDepth, uint32_t clearStencil) {
    static ccstd::vector<GLenum> invalidAttachments;

    GLES2GPUStateCache *cache = device->stateCache();
    GLES2ObjectCache &gfxStateCache = cache->gfxStateCache;

    Rect realRenderArea;
    gfxStateCache.subpassIdx = subpassIdx;
    if (subpassIdx) {
        gpuRenderPass = gfxStateCache.gpuRenderPass;
        gpuFramebuffer = gfxStateCache.gpuFramebuffer;
        realRenderArea = gfxStateCache.renderArea;
        clearColors = gfxStateCache.clearColors.data();
        clearDepth = gfxStateCache.clearDepth;
        clearStencil = gfxStateCache.clearStencil;
    } else {
        realRenderArea.x = renderArea->x;
        realRenderArea.y = renderArea->y;
        realRenderArea.width = renderArea->width << gpuFramebuffer->lodLevel;
        realRenderArea.height = renderArea->height << gpuFramebuffer->lodLevel;

        gfxStateCache.gpuRenderPass = gpuRenderPass;
        gfxStateCache.gpuFramebuffer = gpuFramebuffer;
        gfxStateCache.renderArea = realRenderArea;
        gfxStateCache.clearColors.assign(clearColors, clearColors + gpuRenderPass->colorAttachments.size());
        gfxStateCache.clearDepth = clearDepth;
        gfxStateCache.clearStencil = clearStencil;
    }

    if (gpuFramebuffer && gpuRenderPass) {
        const auto &instance = gpuFramebuffer->usesFBF ? gpuFramebuffer->uberInstance : gpuFramebuffer->instances[subpassIdx];

        GLuint glFramebuffer = instance.framebuffer.getFramebuffer();
        device->context()->makeCurrent(instance.framebuffer.swapchain, instance.framebuffer.swapchain);

        if (cache->glFramebuffer != glFramebuffer) {
            GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, glFramebuffer));
            cache->glFramebuffer = glFramebuffer;
        }

        if (subpassIdx == 0) {
            if (cache->viewport.left != realRenderArea.x ||
                cache->viewport.top != realRenderArea.y ||
                cache->viewport.width != realRenderArea.width ||
                cache->viewport.height != realRenderArea.height) {
                GL_CHECK(glViewport(realRenderArea.x, realRenderArea.y, realRenderArea.width, realRenderArea.height));
                cache->viewport.left = realRenderArea.x;
                cache->viewport.top = realRenderArea.y;
                cache->viewport.width = realRenderArea.width;
                cache->viewport.height = realRenderArea.height;
            }

            if (cache->scissor != realRenderArea) {
                GL_CHECK(glScissor(realRenderArea.x, realRenderArea.y, realRenderArea.width, realRenderArea.height));
                cache->scissor = realRenderArea;
            }
        }

        GLbitfield glClears = 0;
        bool maskSet = false;
        invalidAttachments.clear();

        auto performLoadOp = [&](uint32_t attachmentIndex, uint32_t glAttachmentIndex) {
            if (attachmentIndex == gpuFramebuffer->uberOnChipOutput) attachmentIndex = gpuFramebuffer->uberFinalOutput;

            const ColorAttachment &colorAttachment = gpuRenderPass->colorAttachments[attachmentIndex];
            if (colorAttachment.format != Format::UNKNOWN) {
                switch (colorAttachment.loadOp) {
                    case LoadOp::LOAD: break; // GL default behaviour
                    case LoadOp::CLEAR: {
                        // `glClearColor` clears all the attachments to the same value
                        // here we fallback to the load op of the first attachment
                        // to avoid clearing multiple times
                        if (glAttachmentIndex) break;

                        if (cache->bs.targets[0].blendColorMask != ColorMask::ALL) {
                            GL_CHECK(glColorMask(true, true, true, true));
                            maskSet = true;
                        }

                        const Color &color = clearColors[attachmentIndex];
                        GL_CHECK(glClearColor(color.x, color.y, color.z, color.w));
                        glClears |= GL_COLOR_BUFFER_BIT;
                        break;
                    }
                    case LoadOp::DISCARD:
                        // invalidate fbo
                        invalidAttachments.push_back(glFramebuffer ? GL_COLOR_ATTACHMENT0 + glAttachmentIndex : GL_COLOR_EXT);
                        break;
                }
            }
        };

        auto performDepthStencilLoadOp = [&](uint32_t attachmentIndex, bool skipLoad) {
            if (attachmentIndex != INVALID_BINDING && !skipLoad) {
                LoadOp depthLoadOp = gpuRenderPass->depthStencilAttachment.depthLoadOp;
                LoadOp stencilLoadOp = gpuRenderPass->depthStencilAttachment.stencilLoadOp;
                bool hasStencils = GFX_FORMAT_INFOS[toNumber(gpuRenderPass->depthStencilAttachment.format)].hasStencil;
                if (attachmentIndex < gpuRenderPass->colorAttachments.size()) {
                    depthLoadOp = stencilLoadOp = gpuRenderPass->colorAttachments[attachmentIndex].loadOp;
                    hasStencils = GFX_FORMAT_INFOS[toNumber(gpuRenderPass->colorAttachments[attachmentIndex].format)].hasStencil;
                }

                switch (depthLoadOp) {
                    case LoadOp::LOAD: break; // GL default behaviour
                    case LoadOp::CLEAR: {
                        if (!cache->dss.depthWrite) {
                            GL_CHECK(glDepthMask(true));
                        }
                        GL_CHECK(glClearDepthf(clearDepth));
                        glClears |= GL_DEPTH_BUFFER_BIT;
                    } break;
                    case LoadOp::DISCARD:
                        // invalidate fbo
                        invalidAttachments.push_back(glFramebuffer ? GL_DEPTH_ATTACHMENT : GL_DEPTH_EXT);
                        break;
                }
                if (hasStencils) {
                    switch (depthLoadOp) {
                        case LoadOp::LOAD: break; // GL default behaviour
                        case LoadOp::CLEAR: {
                            if (!cache->dss.stencilWriteMaskFront) {
                                GL_CHECK(glStencilMaskSeparate(GL_FRONT, 0xffffffff));
                            }
                            if (!cache->dss.stencilWriteMaskBack) {
                                GL_CHECK(glStencilMaskSeparate(GL_BACK, 0xffffffff));
                            }
                            GL_CHECK(glClearStencil(clearStencil));
                            glClears |= GL_STENCIL_BUFFER_BIT;
                        } break;
                        case LoadOp::DISCARD:
                            // invalidate fbo
                            invalidAttachments.push_back(glFramebuffer ? GL_STENCIL_ATTACHMENT : GL_STENCIL_EXT);
                            break;
                    }
                }
            }

            if (device->constantRegistry()->useDiscardFramebuffer && !invalidAttachments.empty()) {
                GL_CHECK(glDiscardFramebufferEXT(GL_FRAMEBUFFER, utils::toUint(invalidAttachments.size()), invalidAttachments.data()));
            }

            if (glClears) {
                GL_CHECK(glClear(glClears));
            }

            // restore states
            if (glClears & GL_COLOR_BUFFER_BIT) {
                ColorMask colorMask = cache->bs.targets[0].blendColorMask;
                if (colorMask != ColorMask::ALL) {
                    GL_CHECK(glColorMask((GLboolean)(colorMask & ColorMask::R),
                                         (GLboolean)(colorMask & ColorMask::G),
                                         (GLboolean)(colorMask & ColorMask::B),
                                         (GLboolean)(colorMask & ColorMask::A)));
                }
            }

            if ((glClears & GL_DEPTH_BUFFER_BIT) && !cache->dss.depthWrite) {
                GL_CHECK(glDepthMask(false));
            }

            if (glClears & GL_STENCIL_BUFFER_BIT) {
                if (!cache->dss.stencilWriteMaskFront) {
                    GL_CHECK(glStencilMaskSeparate(GL_FRONT, 0));
                }
                if (!cache->dss.stencilWriteMaskBack) {
                    GL_CHECK(glStencilMaskSeparate(GL_BACK, 0));
                }
            }
        };

        uint32_t glAttachmentIndex = 0U;
        if (gpuFramebuffer->usesFBF) {
            if (subpassIdx == 0) {
                for (const auto attachmentIndex : gpuFramebuffer->uberColorAttachmentIndices) {
                    performLoadOp(attachmentIndex, glAttachmentIndex++);
                }
                performDepthStencilLoadOp(gpuFramebuffer->uberDepthStencil, false);
            }
        } else {
            for (const auto attachmentIndex : gpuRenderPass->subpasses[subpassIdx].colors) {
                if (gpuRenderPass->statistics[attachmentIndex].loadSubpass != subpassIdx) continue;
                performLoadOp(attachmentIndex, glAttachmentIndex++);
            }
            auto depthIndex = gpuRenderPass->subpasses[subpassIdx].depthStencil;
            bool skipLoad = depthIndex == INVALID_BINDING || gpuRenderPass->statistics[depthIndex].loadSubpass != subpassIdx;
            performDepthStencilLoadOp(depthIndex, skipLoad);
        }
    }
}

void cmdFuncGLES2EndRenderPass(GLES2Device *device) {
    static ccstd::vector<GLenum> invalidAttachments;

    GLES2GPUStateCache *cache = device->stateCache();
    GLES2ObjectCache &gfxStateCache = cache->gfxStateCache;
    GLES2GPURenderPass *gpuRenderPass = gfxStateCache.gpuRenderPass;
    GLES2GPUFramebuffer *gpuFramebuffer = gfxStateCache.gpuFramebuffer;
    const auto &instance = gpuFramebuffer->usesFBF ? gpuFramebuffer->uberInstance : gpuFramebuffer->instances[gfxStateCache.subpassIdx];
    const SubpassInfo &subpass = gpuRenderPass->subpasses[gfxStateCache.subpassIdx];
    bool isTheLastSubpass = gfxStateCache.subpassIdx == gpuRenderPass->subpasses.size() - 1;
    GLuint glFramebuffer = instance.framebuffer.getFramebuffer();
    GLuint glResolveFramebuffer = instance.resolveFramebuffer.getFramebuffer();
    bool skipDiscard = false;

    invalidAttachments.clear();
    auto performStoreOp = [&](uint32_t attachmentIndex, uint32_t glAttachmentIndex) {
        if (attachmentIndex == gpuFramebuffer->uberOnChipOutput) attachmentIndex = gpuFramebuffer->uberFinalOutput;

        const ColorAttachment &colorAttachment = gpuRenderPass->colorAttachments[attachmentIndex];
        if (colorAttachment.format != Format::UNKNOWN) {
            switch (colorAttachment.storeOp) {
                case StoreOp::STORE: break;
                case StoreOp::DISCARD:
                    // invalidate fbo
                    invalidAttachments.push_back(glFramebuffer ? GL_COLOR_ATTACHMENT0 + glAttachmentIndex : GL_COLOR_EXT);
                    break;
            }
        }
    };

    auto performDepthStencilStoreOp = [&](uint32_t attachmentIndex, bool skipStore) {
        if (attachmentIndex != INVALID_BINDING && !skipStore) {
            StoreOp depthStoreOp = gpuRenderPass->depthStencilAttachment.depthStoreOp;
            StoreOp stencilStoreOp = gpuRenderPass->depthStencilAttachment.stencilStoreOp;
            bool hasStencils = GFX_FORMAT_INFOS[toNumber(gpuRenderPass->depthStencilAttachment.format)].hasStencil;
            if (attachmentIndex < gpuRenderPass->colorAttachments.size()) {
                depthStoreOp = stencilStoreOp = gpuRenderPass->colorAttachments[attachmentIndex].storeOp;
                hasStencils = GFX_FORMAT_INFOS[toNumber(gpuRenderPass->colorAttachments[attachmentIndex].format)].hasStencil;
            }

            switch (gpuRenderPass->depthStencilAttachment.depthStoreOp) {
                case StoreOp::STORE: break;
                case StoreOp::DISCARD:
                    invalidAttachments.push_back(glFramebuffer ? GL_DEPTH_ATTACHMENT : GL_DEPTH_EXT);
                    break;
            }
            if (hasStencils) {
                switch (gpuRenderPass->depthStencilAttachment.stencilStoreOp) {
                    case StoreOp::STORE: break;
                    case StoreOp::DISCARD:
                        invalidAttachments.push_back(glFramebuffer ? GL_STENCIL_ATTACHMENT : GL_STENCIL_EXT);
                        break;
                }
            }
        }

        if (!skipDiscard && device->constantRegistry()->useDiscardFramebuffer && !invalidAttachments.empty()) {
            GL_CHECK(glDiscardFramebufferEXT(GL_FRAMEBUFFER, utils::toUint(invalidAttachments.size()), invalidAttachments.data()));
        }
    };

    if (instance.resolveMask) {
        device->context()->makeCurrent(instance.resolveFramebuffer.swapchain, instance.framebuffer.swapchain);

        if (cache->glFramebuffer != glResolveFramebuffer) {
            GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, glResolveFramebuffer));
            cache->glFramebuffer = glResolveFramebuffer;
        }

        TextureBlit region;

        if (instance.resolveMask & GL_COLOR_BUFFER_BIT) {
            for (uint32_t i = 0; i < subpass.colors.size(); ++i) {
                GLES2GPUTexture *srcTexture = gpuFramebuffer->gpuColorTextures[subpass.colors[i]];
                GLES2GPUTexture *dstTexture = gpuFramebuffer->gpuColorTextures[subpass.resolves[i]];

                region.srcExtent.width = srcTexture->width;
                region.srcExtent.height = srcTexture->height;
                region.dstExtent.width = dstTexture->width;
                region.dstExtent.height = dstTexture->height;
                device->blitManager()->draw(srcTexture, dstTexture, &region, 1, Filter::POINT);
            }
        }

        if (instance.resolveMask & GL_DEPTH_BUFFER_BIT) {
            GLES2GPUTexture *srcTexture = subpass.depthStencil < gpuFramebuffer->gpuColorTextures.size()
                                              ? gpuFramebuffer->gpuColorTextures[subpass.depthStencil]
                                              : gpuFramebuffer->gpuDepthStencilTexture;
            GLES2GPUTexture *dstTexture = subpass.depthStencilResolve < gpuFramebuffer->gpuColorTextures.size()
                                              ? gpuFramebuffer->gpuColorTextures[subpass.depthStencilResolve]
                                              : gpuFramebuffer->gpuDepthStencilTexture;

            region.srcExtent.width = srcTexture->width;
            region.srcExtent.height = srcTexture->height;
            region.dstExtent.width = dstTexture->width;
            region.dstExtent.height = dstTexture->height;
            device->blitManager()->draw(srcTexture, dstTexture, &region, 1, Filter::POINT);
        }

        skipDiscard = true; // framebuffer was already stored
    }

    uint32_t glAttachmentIndex = 0U;
    if (gpuFramebuffer->usesFBF) {
        if (isTheLastSubpass) {
            for (const auto attachmentIndex : gpuFramebuffer->uberColorAttachmentIndices) {
                performStoreOp(attachmentIndex, glAttachmentIndex++);
            }
            performDepthStencilStoreOp(gpuFramebuffer->uberDepthStencil, false);

            if (gpuFramebuffer->uberOnChipOutput != INVALID_BINDING) {
                TextureBlit region;
                auto *blitSrc = gpuFramebuffer->gpuColorTextures[gpuFramebuffer->uberOnChipOutput];
                auto *blitDst = gpuFramebuffer->gpuColorTextures[gpuFramebuffer->uberFinalOutput];
                region.srcExtent.width = region.dstExtent.width = blitSrc->width;
                region.srcExtent.height = region.dstExtent.height = blitSrc->height;
                cmdFuncGLES2BlitTexture(device, blitSrc, blitDst, &region, 1, Filter::POINT);
            }
        } else {
            if (device->constantRegistry()->mFBF == FBFSupportLevel::NON_COHERENT_EXT) {
                glFramebufferFetchBarrierEXT();
            } else if (device->constantRegistry()->mFBF == FBFSupportLevel::NON_COHERENT_QCOM) {
                glFramebufferFetchBarrierQCOM();
            }
        }
    } else {
        const auto &indices = subpass.resolves.empty() ? subpass.colors : subpass.resolves;
        for (const auto attachmentIndex : indices) {
            if (gpuRenderPass->statistics[attachmentIndex].storeSubpass != gfxStateCache.subpassIdx) continue;
            performStoreOp(attachmentIndex, glAttachmentIndex++);
        }
        bool skipStore = subpass.depthStencil == INVALID_BINDING ||
                         gpuRenderPass->statistics[subpass.depthStencil].storeSubpass != gfxStateCache.subpassIdx;
        performDepthStencilStoreOp(subpass.depthStencil, skipStore);
    }
}

// NOLINTNEXTLINE(google-readability-function-size, readability-function-size)
void cmdFuncGLES2BindState(GLES2Device *device, GLES2GPUPipelineState *gpuPipelineState, GLES2GPUInputAssembler *gpuInputAssembler,
                           const GLES2GPUDescriptorSet *const *gpuDescriptorSets, const uint32_t *dynamicOffsets,
                           const DynamicStates *dynamicStates) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;

    GLES2GPUStateCache *cache = device->stateCache();
    bool isShaderChanged = false;
    GLenum glWrapS = 0U;
    GLenum glWrapT = 0U;
    GLenum glMinFilter = 0U;

    if (gpuPipelineState && gpuPipelineState != gfxStateCache.gpuPipelineState) {
        gfxStateCache.gpuPipelineState = gpuPipelineState;
        gfxStateCache.glPrimitive = gpuPipelineState->glPrimitive;

        if (gpuPipelineState->gpuShader) {
            if (cache->glProgram != gpuPipelineState->gpuShader->glProgram) {
                GL_CHECK(glUseProgram(gpuPipelineState->gpuShader->glProgram));
                cache->glProgram = gpuPipelineState->gpuShader->glProgram;
                isShaderChanged = true;
            }
        }

        // bind rasterizer state
        if (cache->rs.cullMode != gpuPipelineState->rs.cullMode) {
            switch (gpuPipelineState->rs.cullMode) {
                case CullMode::NONE: {
                    if (cache->isCullFaceEnabled) {
                        GL_CHECK(glDisable(GL_CULL_FACE));
                        cache->isCullFaceEnabled = false;
                    }
                } break;
                case CullMode::FRONT: {
                    if (!cache->isCullFaceEnabled) {
                        GL_CHECK(glEnable(GL_CULL_FACE));
                        cache->isCullFaceEnabled = true;
                    }
                    GL_CHECK(glCullFace(GL_FRONT));
                } break;
                case CullMode::BACK: {
                    if (!cache->isCullFaceEnabled) {
                        GL_CHECK(glEnable(GL_CULL_FACE));
                        cache->isCullFaceEnabled = true;
                    }
                    GL_CHECK(glCullFace(GL_BACK));
                } break;
                default:
                    break;
            }
            cache->rs.cullMode = gpuPipelineState->rs.cullMode;
        }
        if (cache->rs.isFrontFaceCCW != gpuPipelineState->rs.isFrontFaceCCW) {
            GL_CHECK(glFrontFace(gpuPipelineState->rs.isFrontFaceCCW ? GL_CCW : GL_CW));
            cache->rs.isFrontFaceCCW = gpuPipelineState->rs.isFrontFaceCCW;
        }
        if ((cache->rs.depthBias != gpuPipelineState->rs.depthBias) ||
            (cache->rs.depthBiasSlop != gpuPipelineState->rs.depthBiasSlop)) {
            GL_CHECK(glPolygonOffset(cache->rs.depthBias, cache->rs.depthBiasSlop));
            cache->rs.depthBiasSlop = gpuPipelineState->rs.depthBiasSlop;
        }
        if (cache->rs.lineWidth != gpuPipelineState->rs.lineWidth) {
            GL_CHECK(glLineWidth(gpuPipelineState->rs.lineWidth));
            cache->rs.lineWidth = gpuPipelineState->rs.lineWidth;
        }

        // bind depth-stencil state
        if (cache->dss.depthTest != gpuPipelineState->dss.depthTest) {
            if (gpuPipelineState->dss.depthTest) {
                GL_CHECK(glEnable(GL_DEPTH_TEST));
            } else {
                GL_CHECK(glDisable(GL_DEPTH_TEST));
            }
            cache->dss.depthTest = gpuPipelineState->dss.depthTest;
        }
        if (cache->dss.depthWrite != gpuPipelineState->dss.depthWrite) {
            GL_CHECK(glDepthMask(!!gpuPipelineState->dss.depthWrite));
            cache->dss.depthWrite = gpuPipelineState->dss.depthWrite;
        }
        if (cache->dss.depthFunc != gpuPipelineState->dss.depthFunc) {
            GL_CHECK(glDepthFunc(GLES2_CMP_FUNCS[(int)gpuPipelineState->dss.depthFunc]));
            cache->dss.depthFunc = gpuPipelineState->dss.depthFunc;
        }

        // bind depth-stencil state - front
        if (gpuPipelineState->dss.stencilTestFront || gpuPipelineState->dss.stencilTestBack) {
            if (!cache->isStencilTestEnabled) {
                GL_CHECK(glEnable(GL_STENCIL_TEST));
                cache->isStencilTestEnabled = true;
            }
        } else {
            if (cache->isStencilTestEnabled) {
                GL_CHECK(glDisable(GL_STENCIL_TEST));
                cache->isStencilTestEnabled = false;
            }
        }
        if (cache->dss.stencilFuncFront != gpuPipelineState->dss.stencilFuncFront ||
            cache->dss.stencilRefFront != gpuPipelineState->dss.stencilRefFront ||
            cache->dss.stencilReadMaskFront != gpuPipelineState->dss.stencilReadMaskFront) {
            GL_CHECK(glStencilFuncSeparate(GL_FRONT,
                                           GLES2_CMP_FUNCS[(int)gpuPipelineState->dss.stencilFuncFront],
                                           gpuPipelineState->dss.stencilRefFront,
                                           gpuPipelineState->dss.stencilReadMaskFront));
            cache->dss.stencilFuncFront = gpuPipelineState->dss.stencilFuncFront;
            cache->dss.stencilRefFront = gpuPipelineState->dss.stencilRefFront;
            cache->dss.stencilReadMaskFront = gpuPipelineState->dss.stencilReadMaskFront;
        }
        if (cache->dss.stencilFailOpFront != gpuPipelineState->dss.stencilFailOpFront ||
            cache->dss.stencilZFailOpFront != gpuPipelineState->dss.stencilZFailOpFront ||
            cache->dss.stencilPassOpFront != gpuPipelineState->dss.stencilPassOpFront) {
            GL_CHECK(glStencilOpSeparate(GL_FRONT,
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilFailOpFront],
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilZFailOpFront],
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilPassOpFront]));
            cache->dss.stencilFailOpFront = gpuPipelineState->dss.stencilFailOpFront;
            cache->dss.stencilZFailOpFront = gpuPipelineState->dss.stencilZFailOpFront;
            cache->dss.stencilPassOpFront = gpuPipelineState->dss.stencilPassOpFront;
        }
        if (cache->dss.stencilWriteMaskFront != gpuPipelineState->dss.stencilWriteMaskFront) {
            GL_CHECK(glStencilMaskSeparate(GL_FRONT, gpuPipelineState->dss.stencilWriteMaskFront));
            cache->dss.stencilWriteMaskFront = gpuPipelineState->dss.stencilWriteMaskFront;
        }

        // bind depth-stencil state - back
        if (cache->dss.stencilFuncBack != gpuPipelineState->dss.stencilFuncBack ||
            cache->dss.stencilRefBack != gpuPipelineState->dss.stencilRefBack ||
            cache->dss.stencilReadMaskBack != gpuPipelineState->dss.stencilReadMaskBack) {
            GL_CHECK(glStencilFuncSeparate(GL_BACK,
                                           GLES2_CMP_FUNCS[(int)gpuPipelineState->dss.stencilFuncBack],
                                           gpuPipelineState->dss.stencilRefBack,
                                           gpuPipelineState->dss.stencilReadMaskBack));
            cache->dss.stencilFuncBack = gpuPipelineState->dss.stencilFuncBack;
            cache->dss.stencilRefBack = gpuPipelineState->dss.stencilRefBack;
            cache->dss.stencilReadMaskBack = gpuPipelineState->dss.stencilReadMaskBack;
        }
        if (cache->dss.stencilFailOpBack != gpuPipelineState->dss.stencilFailOpBack ||
            cache->dss.stencilZFailOpBack != gpuPipelineState->dss.stencilZFailOpBack ||
            cache->dss.stencilPassOpBack != gpuPipelineState->dss.stencilPassOpBack) {
            GL_CHECK(glStencilOpSeparate(GL_BACK,
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilFailOpBack],
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilZFailOpBack],
                                         GLES2_STENCIL_OPS[(int)gpuPipelineState->dss.stencilPassOpBack]));
            cache->dss.stencilFailOpBack = gpuPipelineState->dss.stencilFailOpBack;
            cache->dss.stencilZFailOpBack = gpuPipelineState->dss.stencilZFailOpBack;
            cache->dss.stencilPassOpBack = gpuPipelineState->dss.stencilPassOpBack;
        }
        if (cache->dss.stencilWriteMaskBack != gpuPipelineState->dss.stencilWriteMaskBack) {
            GL_CHECK(glStencilMaskSeparate(GL_BACK, gpuPipelineState->dss.stencilWriteMaskBack));
            cache->dss.stencilWriteMaskBack = gpuPipelineState->dss.stencilWriteMaskBack;
        }

        // bind blend state
        if (cache->bs.isA2C != gpuPipelineState->bs.isA2C) {
            if (cache->bs.isA2C) {
                GL_CHECK(glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE));
            } else {
                GL_CHECK(glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE));
            }
            cache->bs.isA2C = gpuPipelineState->bs.isA2C;
        }
        if (cache->bs.blendColor.x != gpuPipelineState->bs.blendColor.x ||
            cache->bs.blendColor.y != gpuPipelineState->bs.blendColor.y ||
            cache->bs.blendColor.z != gpuPipelineState->bs.blendColor.z ||
            cache->bs.blendColor.w != gpuPipelineState->bs.blendColor.w) {
            GL_CHECK(glBlendColor(gpuPipelineState->bs.blendColor.x,
                                  gpuPipelineState->bs.blendColor.y,
                                  gpuPipelineState->bs.blendColor.z,
                                  gpuPipelineState->bs.blendColor.w));
            cache->bs.blendColor = gpuPipelineState->bs.blendColor;
        }

        if (!gpuPipelineState->bs.targets.empty()) {
            BlendTarget &cacheTarget = cache->bs.targets[0];
            const BlendTarget &target = gpuPipelineState->bs.targets[0];
            if (cacheTarget.blend != target.blend) {
                if (!cacheTarget.blend) {
                    GL_CHECK(glEnable(GL_BLEND));
                } else {
                    GL_CHECK(glDisable(GL_BLEND));
                }
                cacheTarget.blend = target.blend;
            }
            if (cacheTarget.blendEq != target.blendEq ||
                cacheTarget.blendAlphaEq != target.blendAlphaEq) {
                GL_CHECK(glBlendEquationSeparate(GLES2_BLEND_OPS[(int)target.blendEq],
                                                 GLES2_BLEND_OPS[(int)target.blendAlphaEq]));
                cacheTarget.blendEq = target.blendEq;
                cacheTarget.blendAlphaEq = target.blendAlphaEq;
            }
            if (cacheTarget.blendSrc != target.blendSrc ||
                cacheTarget.blendDst != target.blendDst ||
                cacheTarget.blendSrcAlpha != target.blendSrcAlpha ||
                cacheTarget.blendDstAlpha != target.blendDstAlpha) {
                GL_CHECK(glBlendFuncSeparate(GLES2_BLEND_FACTORS[(int)target.blendSrc],
                                             GLES2_BLEND_FACTORS[(int)target.blendDst],
                                             GLES2_BLEND_FACTORS[(int)target.blendSrcAlpha],
                                             GLES2_BLEND_FACTORS[(int)target.blendDstAlpha]));
                cacheTarget.blendSrc = target.blendSrc;
                cacheTarget.blendDst = target.blendDst;
                cacheTarget.blendSrcAlpha = target.blendSrcAlpha;
                cacheTarget.blendDstAlpha = target.blendDstAlpha;
            }
            if (cacheTarget.blendColorMask != target.blendColorMask) {
                GL_CHECK(glColorMask((GLboolean)(target.blendColorMask & ColorMask::R),
                                     (GLboolean)(target.blendColorMask & ColorMask::G),
                                     (GLboolean)(target.blendColorMask & ColorMask::B),
                                     (GLboolean)(target.blendColorMask & ColorMask::A)));
                cacheTarget.blendColorMask = target.blendColorMask;
            }
        }
    } // if

    // bind descriptor sets
    if (gpuPipelineState && gpuPipelineState->gpuShader && gpuPipelineState->gpuPipelineLayout) {
        size_t blockLen{gpuPipelineState->gpuShader->glBlocks.size()};
        const ccstd::vector<ccstd::vector<int>> &dynamicOffsetIndices{gpuPipelineState->gpuPipelineLayout->dynamicOffsetIndices};
        uint8_t *uniformBuffBase{nullptr};
        uint8_t *uniformBuff{nullptr};
        uint8_t *uniformCachedBuff{nullptr};

        for (size_t j = 0; j < blockLen; j++) {
            GLES2GPUUniformBlock &glBlock = gpuPipelineState->gpuShader->glBlocks[j];

            const GLES2GPUDescriptorSet *gpuDescriptorSet = gpuDescriptorSets[glBlock.set];
            const uint32_t descriptorIndex = gpuDescriptorSet->descriptorIndices->at(glBlock.binding);
            const GLES2GPUDescriptor &gpuDescriptor = gpuDescriptorSet->gpuDescriptors[descriptorIndex];

            if (!gpuDescriptor.gpuBuffer && !gpuDescriptor.gpuBufferView) {
                // CC_LOG_ERROR("Buffer binding '%s' at set %d binding %d is not bounded",
                //              glBlock.name.c_str(), glBlock.set, glBlock.binding);
                continue;
            }

            uint32_t offset = 0U;
            const ccstd::vector<int> &dynamicOffsetIndexSet = dynamicOffsetIndices[glBlock.set];
            if (dynamicOffsetIndexSet.size() > glBlock.binding) {
                int dynamicOffsetIndex = dynamicOffsetIndexSet[glBlock.binding];
                if (dynamicOffsetIndex >= 0) offset = dynamicOffsets[dynamicOffsetIndex];
            }

            if (gpuDescriptor.gpuBufferView) {
                uniformBuffBase = gpuDescriptor.gpuBufferView->gpuBuffer->buffer +
                                  gpuDescriptor.gpuBufferView->offset + offset;
            } else if (gpuDescriptor.gpuBuffer) {
                uniformBuffBase = gpuDescriptor.gpuBuffer->buffer + offset;
            }

            for (auto &gpuUniform : glBlock.glActiveUniforms) {
                uniformBuff = uniformBuffBase + gpuUniform.offset;
                uniformCachedBuff = gpuUniform.buff.data();

                switch (gpuUniform.glType) {
                    case GL_BOOL:
                    case GL_INT: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform1iv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLint *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_BOOL_VEC2:
                    case GL_INT_VEC2: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform2iv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLint *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_BOOL_VEC3:
                    case GL_INT_VEC3: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform3iv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLint *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_BOOL_VEC4:
                    case GL_INT_VEC4: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform4iv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLint *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform1fv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_VEC2: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform2fv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_VEC3: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform3fv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_VEC4: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniform4fv(gpuUniform.glLoc, gpuUniform.count, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_MAT2: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniformMatrix2fv(gpuUniform.glLoc, gpuUniform.count, GL_FALSE, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_MAT3: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniformMatrix3fv(gpuUniform.glLoc, gpuUniform.count, GL_FALSE, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    case GL_FLOAT_MAT4: {
                        if (memcmp(uniformCachedBuff, uniformBuff, gpuUniform.size) != 0) {
                            GL_CHECK(glUniformMatrix4fv(gpuUniform.glLoc, gpuUniform.count, GL_FALSE, reinterpret_cast<const GLfloat *>(uniformBuff)));
                            memcpy(uniformCachedBuff, uniformBuff, gpuUniform.size);
                        }
                        break;
                    }
                    default:
                        break;
                }
            }
        }

        size_t samplerLen = gpuPipelineState->gpuShader->glSamplerTextures.size();
        for (size_t j = 0; j < samplerLen; j++) {
            const GLES2GPUUniformSamplerTexture &glSamplerTexture = gpuPipelineState->gpuShader->glSamplerTextures[j];

            const GLES2GPUDescriptorSet *gpuDescriptorSet = gpuDescriptorSets[glSamplerTexture.set];
            const uint32_t descriptorIndex = gpuDescriptorSet->descriptorIndices->at(glSamplerTexture.binding);
            const GLES2GPUDescriptor *gpuDescriptor = &gpuDescriptorSet->gpuDescriptors[descriptorIndex];

            for (size_t u = 0; u < glSamplerTexture.units.size(); u++, gpuDescriptor++) {
                auto unit = static_cast<uint32_t>(glSamplerTexture.units[u]);

                if (!gpuDescriptor->gpuTexture || !gpuDescriptor->gpuSampler) {
                    // CC_LOG_ERROR("Sampler texture '%s' at set %d binding %d index %d is not bounded",
                    //              glSamplerTexture.name.c_str(), glSamplerTexture.set, glSamplerTexture.binding, u);
                    continue;
                }

                GLES2GPUTexture *gpuTexture = gpuDescriptor->gpuTexture;
                GLuint glTexture = gpuTexture->glTexture;
                CC_ASSERT(!gpuTexture->glRenderbuffer); // Can not sample renderbuffers!

                if (cache->glTextures[unit] != glTexture) {
                    if (cache->texUint != unit) {
                        GL_CHECK(glActiveTexture(GL_TEXTURE0 + unit));
                        cache->texUint = unit;
                    }
                    GL_CHECK(glBindTexture(gpuTexture->glTarget, glTexture));
                    cache->glTextures[unit] = glTexture;
                }

                if (gpuDescriptor->gpuTexture->isPowerOf2) {
                    glWrapS = gpuDescriptor->gpuSampler->glWrapS;
                    glWrapT = gpuDescriptor->gpuSampler->glWrapT;

                    if (gpuDescriptor->gpuTexture->mipLevel <= 1 &&
                        !hasFlag(gpuDescriptor->gpuTexture->flags, TextureFlagBit::GEN_MIPMAP) &&
                        (gpuDescriptor->gpuSampler->glMinFilter == GL_LINEAR_MIPMAP_NEAREST ||
                         gpuDescriptor->gpuSampler->glMinFilter == GL_LINEAR_MIPMAP_LINEAR)) {
                        glMinFilter = GL_LINEAR;
                    } else {
                        glMinFilter = gpuDescriptor->gpuSampler->glMinFilter;
                    }
                } else {
                    glWrapS = GL_CLAMP_TO_EDGE;
                    glWrapT = GL_CLAMP_TO_EDGE;

                    if (gpuDescriptor->gpuSampler->glMinFilter == GL_LINEAR ||
                        gpuDescriptor->gpuSampler->glMinFilter == GL_LINEAR_MIPMAP_NEAREST ||
                        gpuDescriptor->gpuSampler->glMinFilter == GL_LINEAR_MIPMAP_LINEAR) {
                        glMinFilter = GL_LINEAR;
                    } else {
                        glMinFilter = GL_NEAREST;
                    }
                }

                if (gpuTexture->glWrapS != glWrapS) {
                    if (cache->texUint != unit) {
                        GL_CHECK(glActiveTexture(GL_TEXTURE0 + unit));
                        cache->texUint = unit;
                    }

                    GL_CHECK(glTexParameteri(gpuTexture->glTarget, GL_TEXTURE_WRAP_S, glWrapS));
                    gpuTexture->glWrapS = glWrapS;
                }

                if (gpuTexture->glWrapT != glWrapT) {
                    if (cache->texUint != unit) {
                        GL_CHECK(glActiveTexture(GL_TEXTURE0 + unit));
                        cache->texUint = unit;
                    }
                    GL_CHECK(glTexParameteri(gpuTexture->glTarget, GL_TEXTURE_WRAP_T, glWrapT));
                    gpuTexture->glWrapT = glWrapT;
                }

                if (gpuTexture->glMinFilter != glMinFilter) {
                    if (cache->texUint != unit) {
                        GL_CHECK(glActiveTexture(GL_TEXTURE0 + unit));
                        cache->texUint = unit;
                    }
                    GL_CHECK(glTexParameteri(gpuTexture->glTarget, GL_TEXTURE_MIN_FILTER, glMinFilter));
                    gpuTexture->glMinFilter = glMinFilter;
                }

                if (gpuTexture->glMagFilter != gpuDescriptor->gpuSampler->glMagFilter) {
                    if (cache->texUint != unit) {
                        GL_CHECK(glActiveTexture(GL_TEXTURE0 + unit));
                        cache->texUint = unit;
                    }
                    GL_CHECK(glTexParameteri(gpuTexture->glTarget, GL_TEXTURE_MAG_FILTER, gpuDescriptor->gpuSampler->glMagFilter));
                    gpuTexture->glMagFilter = gpuDescriptor->gpuSampler->glMagFilter;
                }
            }
        }
    } // if

    // bind vao
    if (gpuInputAssembler && gpuPipelineState && gpuPipelineState->gpuShader &&
        (isShaderChanged || gpuInputAssembler != gfxStateCache.gpuInputAssembler)) {
        gfxStateCache.gpuInputAssembler = gpuInputAssembler;
        if (device->constantRegistry()->useVAO) {
            size_t hash = gpuPipelineState->gpuShader->glProgram ^ device->constantRegistry()->currentBoundThreadID;
            GLuint glVAO = gpuInputAssembler->glVAOs[hash];
            if (!glVAO) {
                GL_CHECK(glGenVertexArraysOES(1, &glVAO));
                gpuInputAssembler->glVAOs[hash] = glVAO;
                GL_CHECK(glBindVertexArrayOES(glVAO));
                GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
                GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

                for (auto &gpuInput : gpuPipelineState->gpuShader->glInputs) {
                    for (size_t a = 0; a < gpuInputAssembler->attributes.size(); ++a) {
                        const GLES2GPUAttribute &gpuAttribute = gpuInputAssembler->glAttribs[a];
                        if (gpuAttribute.name == gpuInput.name) {
                            GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, gpuAttribute.glBuffer));

                            for (uint32_t c = 0; c < gpuAttribute.componentCount; ++c) {
                                GLuint glLoc = gpuInput.glLoc + c;
                                uint32_t attribOffset = gpuAttribute.offset + gpuAttribute.size * c;
                                GL_CHECK(glEnableVertexAttribArray(glLoc));

                                cache->glEnabledAttribLocs[glLoc] = true;
                                GL_CHECK(glVertexAttribPointer(glLoc, gpuAttribute.count,
                                                               gpuAttribute.glType,
                                                               gpuAttribute.isNormalized,
                                                               gpuAttribute.stride,
                                                               BUFFER_OFFSET(attribOffset)));

                                if (device->constantRegistry()->useInstancedArrays) {
                                    GL_CHECK(glVertexAttribDivisorEXT(glLoc, gpuAttribute.isInstanced ? 1 : 0));
                                }
                            }
                            break;
                        }
                    } // for
                }     // for

                if (gpuInputAssembler->gpuIndexBuffer) {
                    GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuInputAssembler->gpuIndexBuffer->glBuffer));
                }

                GL_CHECK(glBindVertexArrayOES(0));
                GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
                GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
                cache->glVAO = 0;
                cache->glArrayBuffer = 0;
                cache->glElementArrayBuffer = 0;
            }

            if (cache->glVAO != glVAO) {
                GL_CHECK(glBindVertexArrayOES(glVAO));
                cache->glVAO = glVAO;
            }
        } else {
            for (auto &&glCurrentAttribLoc : cache->glCurrentAttribLocs) {
                glCurrentAttribLoc = false;
            }

            for (auto &gpuInput : gpuPipelineState->gpuShader->glInputs) {
                for (size_t a = 0; a < gpuInputAssembler->attributes.size(); ++a) {
                    const GLES2GPUAttribute &gpuAttribute = gpuInputAssembler->glAttribs[a];
                    if (gpuAttribute.name == gpuInput.name) {
                        if (cache->glArrayBuffer != gpuAttribute.glBuffer) {
                            GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, gpuAttribute.glBuffer));
                            cache->glArrayBuffer = gpuAttribute.glBuffer;
                        }

                        for (uint32_t c = 0; c < gpuAttribute.componentCount; ++c) {
                            GLuint glLoc = gpuInput.glLoc + c;
                            uint32_t attribOffset = gpuAttribute.offset + gpuAttribute.size * c;
                            GL_CHECK(glEnableVertexAttribArray(glLoc));
                            cache->glEnabledAttribLocs[glLoc] = true;
                            cache->glCurrentAttribLocs[glLoc] = true;
                            GL_CHECK(glVertexAttribPointer(glLoc, gpuAttribute.count,
                                                           gpuAttribute.glType,
                                                           gpuAttribute.isNormalized,
                                                           gpuAttribute.stride,
                                                           BUFFER_OFFSET(attribOffset)));

                            if (device->constantRegistry()->useInstancedArrays) {
                                GL_CHECK(glVertexAttribDivisorEXT(glLoc, gpuAttribute.isInstanced ? 1 : 0));
                            }
                        }
                        break;
                    }
                }
            }

            if (gpuInputAssembler->gpuIndexBuffer) {
                if (cache->glElementArrayBuffer !=
                    gpuInputAssembler->gpuIndexBuffer->glBuffer) {
                    GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuInputAssembler->gpuIndexBuffer->glBuffer));
                    cache->glElementArrayBuffer = gpuInputAssembler->gpuIndexBuffer->glBuffer;
                }
            }

            for (uint32_t a = 0; a < cache->glCurrentAttribLocs.size(); ++a) {
                if (cache->glEnabledAttribLocs[a] != cache->glCurrentAttribLocs[a]) {
                    GL_CHECK(glDisableVertexAttribArray(a));
                    cache->glEnabledAttribLocs[a] = false;
                }
            }
        }
    }

    if (gpuPipelineState && !gpuPipelineState->dynamicStates.empty()) {
        for (DynamicStateFlagBit dynamicState : gpuPipelineState->dynamicStates) {
            switch (dynamicState) {
                case DynamicStateFlagBit::LINE_WIDTH:
                    if (cache->rs.lineWidth != dynamicStates->lineWidth) {
                        cache->rs.lineWidth = dynamicStates->lineWidth;
                        GL_CHECK(glLineWidth(dynamicStates->lineWidth));
                    }
                    break;
                case DynamicStateFlagBit::DEPTH_BIAS:
                    if ((cache->rs.depthBias != dynamicStates->depthBiasConstant) ||
                        (cache->rs.depthBiasSlop != dynamicStates->depthBiasSlope)) {
                        GL_CHECK(glPolygonOffset(dynamicStates->depthBiasConstant, dynamicStates->depthBiasSlope));
                        cache->rs.depthBias = dynamicStates->depthBiasConstant;
                        cache->rs.depthBiasSlop = dynamicStates->depthBiasSlope;
                    }
                    break;
                case DynamicStateFlagBit::BLEND_CONSTANTS:
                    if ((cache->bs.blendColor.x != dynamicStates->blendConstant.x) ||
                        (cache->bs.blendColor.y != dynamicStates->blendConstant.y) ||
                        (cache->bs.blendColor.z != dynamicStates->blendConstant.z) ||
                        (cache->bs.blendColor.w != dynamicStates->blendConstant.w)) {
                        GL_CHECK(glBlendColor(dynamicStates->blendConstant.x,
                                              dynamicStates->blendConstant.y,
                                              dynamicStates->blendConstant.z,
                                              dynamicStates->blendConstant.w));
                        cache->bs.blendColor = dynamicStates->blendConstant;
                    }
                    break;
                case DynamicStateFlagBit::STENCIL_WRITE_MASK: {
                    const auto &front = dynamicStates->stencilStatesFront;
                    const auto &back = dynamicStates->stencilStatesBack;
                    if (cache->dss.stencilWriteMaskFront != front.writeMask) {
                        GL_CHECK(glStencilMaskSeparate(GL_FRONT, front.writeMask));
                        cache->dss.stencilWriteMaskFront = front.writeMask;
                    }
                    if (cache->dss.stencilWriteMaskBack != back.writeMask) {
                        GL_CHECK(glStencilMaskSeparate(GL_BACK, back.writeMask));
                        cache->dss.stencilWriteMaskBack = back.writeMask;
                    }
                } break;
                case DynamicStateFlagBit::STENCIL_COMPARE_MASK: {
                    const auto &front = dynamicStates->stencilStatesFront;
                    const auto &back = dynamicStates->stencilStatesBack;
                    if ((cache->dss.stencilRefFront != front.reference) ||
                        (cache->dss.stencilReadMaskFront != front.compareMask)) {
                        GL_CHECK(glStencilFuncSeparate(GL_FRONT,
                                                       GLES2_CMP_FUNCS[toNumber(cache->dss.stencilFuncFront)],
                                                       front.reference,
                                                       front.compareMask));
                        cache->dss.stencilRefFront = front.reference;
                        cache->dss.stencilReadMaskFront = front.compareMask;
                    }
                    if ((cache->dss.stencilRefBack != back.reference) ||
                        (cache->dss.stencilReadMaskBack != back.compareMask)) {
                        GL_CHECK(glStencilFuncSeparate(GL_BACK,
                                                       GLES2_CMP_FUNCS[toNumber(cache->dss.stencilFuncBack)],
                                                       back.reference,
                                                       back.compareMask));
                        cache->dss.stencilRefBack = back.reference;
                        cache->dss.stencilReadMaskBack = back.compareMask;
                    }
                } break;
                default:
                    CC_LOG_ERROR("Invalid dynamic states.");
                    break;
            }
        }
    }
}

void cmdFuncGLES2Draw(GLES2Device *device, const DrawInfo &drawInfo) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    GLES2GPUPipelineState *gpuPipelineState = gfxStateCache.gpuPipelineState;
    GLES2GPUInputAssembler *gpuInputAssembler = gfxStateCache.gpuInputAssembler;
    GLenum glPrimitive = gfxStateCache.glPrimitive;

    if (gpuInputAssembler && gpuPipelineState) {
        if (!gpuInputAssembler->gpuIndirectBuffer) {
            if (gpuInputAssembler->gpuIndexBuffer) {
                if (drawInfo.indexCount > 0) {
                    uint8_t *offset = nullptr;
                    offset += drawInfo.firstIndex * gpuInputAssembler->gpuIndexBuffer->stride;
                    if (drawInfo.instanceCount == 0) {
                        GL_CHECK(glDrawElements(glPrimitive, drawInfo.indexCount, gpuInputAssembler->glIndexType, offset));
                    } else {
                        if (device->constantRegistry()->useDrawInstanced) {
                            GL_CHECK(glDrawElementsInstancedEXT(glPrimitive, drawInfo.indexCount, gpuInputAssembler->glIndexType, offset, drawInfo.instanceCount));
                        }
                    }
                }
            } else if (drawInfo.vertexCount > 0) {
                if (drawInfo.instanceCount == 0) {
                    GL_CHECK(glDrawArrays(glPrimitive, drawInfo.firstVertex, drawInfo.vertexCount));
                } else {
                    if (device->constantRegistry()->useDrawInstanced) {
                        GL_CHECK(glDrawArraysInstancedEXT(glPrimitive, drawInfo.firstVertex, drawInfo.vertexCount, drawInfo.instanceCount));
                    }
                }
            }
        } else {
            for (size_t j = 0; j < gpuInputAssembler->gpuIndirectBuffer->indirects.size(); ++j) {
                const DrawInfo &draw = gpuInputAssembler->gpuIndirectBuffer->indirects[j];
                if (gpuInputAssembler->gpuIndexBuffer) {
                    if (draw.indexCount > 0) {
                        uint8_t *offset = nullptr;
                        offset += draw.firstIndex * gpuInputAssembler->gpuIndexBuffer->stride;
                        if (drawInfo.instanceCount == 0) {
                            GL_CHECK(glDrawElements(glPrimitive, draw.indexCount, gpuInputAssembler->glIndexType, offset));
                        } else {
                            if (device->constantRegistry()->useDrawInstanced) {
                                GL_CHECK(glDrawElementsInstancedEXT(glPrimitive, draw.indexCount, gpuInputAssembler->glIndexType, offset, draw.instanceCount));
                            }
                        }
                    }
                } else if (draw.vertexCount > 0) {
                    if (draw.instanceCount == 0) {
                        GL_CHECK(glDrawArrays(glPrimitive, draw.firstVertex, draw.vertexCount));
                    } else {
                        if (device->constantRegistry()->useDrawInstanced) {
                            GL_CHECK(glDrawArraysInstancedEXT(glPrimitive, draw.firstVertex, draw.vertexCount, draw.instanceCount));
                        }
                    }
                }
            }
        }
    }
}

void cmdFuncGLES2UpdateBuffer(GLES2Device *device, GLES2GPUBuffer *gpuBuffer, const void *buffer, uint32_t offset, uint32_t size) {
    GLES2ObjectCache &gfxStateCache = device->stateCache()->gfxStateCache;
    CC_ASSERT(buffer);
    if ((hasFlag(gpuBuffer->usage, BufferUsageBit::UNIFORM)) ||
        (hasFlag(gpuBuffer->usage, BufferUsageBit::TRANSFER_SRC))) {
        memcpy(gpuBuffer->buffer + offset, buffer, size);
    } else if (hasFlag(gpuBuffer->usage, BufferUsageBit::INDIRECT)) {
        memcpy(reinterpret_cast<uint8_t *>(gpuBuffer->indirects.data()) + offset, buffer, size);
    } else {
        switch (gpuBuffer->glTarget) {
            case GL_ARRAY_BUFFER: {
                if (device->constantRegistry()->useVAO) {
                    if (device->stateCache()->glVAO) {
                        GL_CHECK(glBindVertexArrayOES(0));
                        device->stateCache()->glVAO = 0;
                    }
                }
                gfxStateCache.gpuInputAssembler = nullptr;
                if (device->stateCache()->glArrayBuffer != gpuBuffer->glBuffer) {
                    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, gpuBuffer->glBuffer));
                    device->stateCache()->glArrayBuffer = gpuBuffer->glBuffer;
                }
                GL_CHECK(glBufferSubData(GL_ARRAY_BUFFER, offset, size, buffer));
                break;
            }
            case GL_ELEMENT_ARRAY_BUFFER: {
                if (device->constantRegistry()->useVAO) {
                    if (device->stateCache()->glVAO) {
                        GL_CHECK(glBindVertexArrayOES(0));
                        device->stateCache()->glVAO = 0;
                    }
                }
                gfxStateCache.gpuInputAssembler = nullptr;
                if (device->stateCache()->glElementArrayBuffer != gpuBuffer->glBuffer) {
                    GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuBuffer->glBuffer));
                    device->stateCache()->glElementArrayBuffer = gpuBuffer->glBuffer;
                }
                GL_CHECK(glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, size, buffer));
                break;
            }
            default:
                CC_ABORT();
                break;
        }
    }
}

uint8_t *funcGLES2PixelBufferPick(const uint8_t *buffer, Format format, uint32_t offset, Extent stride, Extent extent) {
    const auto blockHeight = formatAlignment(format).second;

    const uint32_t bufferSize = formatSize(format, extent.width, extent.height, extent.depth);
    const uint32_t rowStrideSize = formatSize(format, stride.width, 1, 1);
    const uint32_t sliceStrideSize = formatSize(format, stride.width, stride.height, 1);
    const uint32_t destRowSize = formatSize(format, extent.width, 1, 1);

    uint8_t *stagingBuffer = GLES2Device::getInstance()->getStagingBuffer(bufferSize);

    uint32_t bufferOffset = 0;
    uint32_t destOffset = 0;

    for (uint32_t i = 0; i < extent.depth; i++) {
        bufferOffset = offset + sliceStrideSize * i;
        for (uint32_t j = 0; j < extent.height; j += blockHeight) {
            memcpy(stagingBuffer + destOffset, buffer + bufferOffset, destRowSize);
            destOffset += destRowSize;
            bufferOffset += rowStrideSize;
        }
    }

    return stagingBuffer;
}

void cmdFuncGLES2CopyBuffersToTexture(GLES2Device *device, const uint8_t *const *buffers,
                                      GLES2GPUTexture *gpuTexture, const BufferTextureCopy *regions, uint32_t count) {
    GLuint &glTexture = device->stateCache()->glTextures[device->stateCache()->texUint];
    if (glTexture != gpuTexture->glTexture) {
        GL_CHECK(glBindTexture(gpuTexture->glTarget, gpuTexture->glTexture));
        glTexture = gpuTexture->glTexture;
    }

    bool isCompressed = GFX_FORMAT_INFOS[static_cast<int>(gpuTexture->format)].isCompressed;
    uint32_t n = 0;

    const auto blockSize = formatAlignment(gpuTexture->format);

    uint32_t destWidth = 0;
    uint32_t destHeight = 0;

    switch (gpuTexture->glTarget) {
        case GL_TEXTURE_2D: {
            Extent extent{};
            Offset offset{};
            Extent stride{};
            for (size_t i = 0; i < count; ++i) {
                const BufferTextureCopy &region = regions[i];
                uint32_t mipLevel = region.texSubres.mipLevel;

                offset.x = region.texOffset.x == 0 ? 0 : utils::alignTo(region.texOffset.x, static_cast<int32_t>(blockSize.first));
                offset.y = region.texOffset.y == 0 ? 0 : utils::alignTo(region.texOffset.y, static_cast<int32_t>(blockSize.second));
                extent.width = region.texExtent.width < blockSize.first ? region.texExtent.width : utils::alignTo(region.texExtent.width, static_cast<uint32_t>(blockSize.first));
                extent.height = region.texExtent.height < blockSize.second ? region.texExtent.height : utils::alignTo(region.texExtent.height, static_cast<uint32_t>(blockSize.second));
                stride.width = region.buffStride > 0 ? region.buffStride : extent.width;
                stride.height = region.buffTexHeight > 0 ? region.buffTexHeight : extent.height;

                destWidth = (region.texExtent.width + offset.x == (gpuTexture->width >> mipLevel)) ? region.texExtent.width : extent.width;
                destHeight = (region.texExtent.height + offset.y == (gpuTexture->height >> mipLevel)) ? region.texExtent.height : extent.height;

                const uint8_t *buff;
                if (stride.width == extent.width && stride.height == extent.height) {
                    buff = buffers[n++] + region.buffOffset;
                } else {
                    buff = funcGLES2PixelBufferPick(buffers[n++], gpuTexture->format, region.buffOffset, stride, extent);
                }

                if (isCompressed) {
                    auto memSize = static_cast<GLsizei>(formatSize(gpuTexture->format, extent.width, extent.height, 1));
                    GL_CHECK(glCompressedTexSubImage2D(GL_TEXTURE_2D,
                                                       mipLevel,
                                                       offset.x, offset.y,
                                                       destWidth, destHeight,
                                                       gpuTexture->glFormat,
                                                       memSize,
                                                       (GLvoid *)buff));
                } else {
                    GL_CHECK(glTexSubImage2D(GL_TEXTURE_2D,
                                             mipLevel,
                                             offset.x, offset.y,
                                             destWidth, destHeight,
                                             gpuTexture->glFormat,
                                             gpuTexture->glType,
                                             (GLvoid *)buff));
                }
            }
            break;
        }
        case GL_TEXTURE_2D_ARRAY: {
            Extent extent{};
            Offset offset{};
            Extent stride{};
            for (size_t i = 0; i < count; ++i) {
                const BufferTextureCopy &region = regions[i];
                uint32_t d = region.texSubres.layerCount;
                uint32_t layerCount = d + region.texSubres.baseArrayLayer;
                uint32_t mipLevel = region.texSubres.mipLevel;

                offset.x = region.texOffset.x == 0 ? 0 : utils::alignTo(region.texOffset.x, static_cast<int32_t>(blockSize.first));
                offset.y = region.texOffset.y == 0 ? 0 : utils::alignTo(region.texOffset.y, static_cast<int32_t>(blockSize.second));
                extent.width = region.texExtent.width < blockSize.first ? region.texExtent.width : utils::alignTo(region.texExtent.width, static_cast<uint32_t>(blockSize.first));
                extent.height = region.texExtent.height < blockSize.second ? region.texExtent.height : utils::alignTo(region.texExtent.height, static_cast<uint32_t>(blockSize.second));
                extent.depth = 1; // 1 slice/loop
                stride.width = region.buffStride > 0 ? region.buffStride : extent.width;
                stride.height = region.buffTexHeight > 0 ? region.buffTexHeight : extent.height;

                destWidth = (region.texExtent.width + offset.x == (gpuTexture->width >> mipLevel)) ? region.texExtent.width : extent.width;
                destHeight = (region.texExtent.height + offset.y == (gpuTexture->height >> mipLevel)) ? region.texExtent.height : extent.height;

                for (uint32_t z = region.texSubres.baseArrayLayer; z < layerCount; ++z) {
                    offset.z = z;

                    const uint8_t *buff;
                    if (stride.width == extent.width && stride.height == extent.height) {
                        buff = buffers[n++] + region.buffOffset;
                    } else {
                        buff = funcGLES2PixelBufferPick(buffers[n++], gpuTexture->format, region.buffOffset, stride, extent);
                    }

                    if (isCompressed) {
                        auto memSize = static_cast<GLsizei>(formatSize(gpuTexture->format, extent.width, extent.height, 1));
                        GL_CHECK(glCompressedTexSubImage3DOES(GL_TEXTURE_2D_ARRAY,
                                                              mipLevel,
                                                              offset.x, offset.y, offset.z,
                                                              destWidth, destHeight, extent.depth,
                                                              gpuTexture->glFormat,
                                                              memSize,
                                                              (GLvoid *)buff));
                    } else {
                        GL_CHECK(glTexSubImage3DOES(GL_TEXTURE_2D_ARRAY,
                                                    mipLevel,
                                                    offset.x, offset.y, offset.z,
                                                    destWidth, destHeight, extent.depth,
                                                    gpuTexture->glFormat,
                                                    gpuTexture->glType,
                                                    (GLvoid *)buff));
                    }
                }
            }
            break;
        }
        case GL_TEXTURE_3D: {
            Extent extent{};
            Offset offset{};
            Extent stride{};
            for (size_t i = 0; i < count; ++i) {
                const BufferTextureCopy &region = regions[i];
                uint32_t mipLevel = region.texSubres.mipLevel;

                offset.x = region.texOffset.x == 0 ? 0 : utils::alignTo(region.texOffset.x, static_cast<int32_t>(blockSize.first));
                offset.y = region.texOffset.y == 0 ? 0 : utils::alignTo(region.texOffset.y, static_cast<int32_t>(blockSize.second));
                offset.z = region.texOffset.z;
                extent.width = region.texExtent.width < blockSize.first ? region.texExtent.width : utils::alignTo(region.texExtent.width, static_cast<uint32_t>(blockSize.first));
                extent.height = region.texExtent.height < blockSize.second ? region.texExtent.height : utils::alignTo(region.texExtent.height, static_cast<uint32_t>(blockSize.second));
                extent.depth = region.texExtent.depth;
                stride.width = region.buffStride > 0 ? region.buffStride : extent.width;
                stride.height = region.buffTexHeight > 0 ? region.buffTexHeight : extent.height;

                destWidth = (region.texExtent.width + offset.x == (gpuTexture->width >> mipLevel)) ? region.texExtent.width : extent.width;
                destHeight = (region.texExtent.height + offset.y == (gpuTexture->height >> mipLevel)) ? region.texExtent.height : extent.height;

                const uint8_t *buff;
                if (stride.width == extent.width && stride.height == extent.height) {
                    buff = buffers[n++] + region.buffOffset;
                } else {
                    buff = funcGLES2PixelBufferPick(buffers[n++], gpuTexture->format, region.buffOffset, stride, extent);
                }
                if (isCompressed) {
                    auto memSize = static_cast<GLsizei>(formatSize(gpuTexture->format, extent.width, extent.height, extent.depth));
                    GL_CHECK(glCompressedTexSubImage3DOES(GL_TEXTURE_3D,
                                                          mipLevel,
                                                          offset.x, offset.y, offset.z,
                                                          destWidth, destHeight, extent.depth,
                                                          gpuTexture->glFormat,
                                                          memSize,
                                                          (GLvoid *)buff));
                } else {
                    GL_CHECK(glTexSubImage3DOES(GL_TEXTURE_3D,
                                                mipLevel,
                                                offset.x, offset.y, offset.z,
                                                destWidth, destHeight, extent.depth,
                                                gpuTexture->glFormat,
                                                gpuTexture->glType,
                                                (GLvoid *)buff));
                }
            }
            break;
        }
        case GL_TEXTURE_CUBE_MAP: {
            Extent extent{};
            Offset offset{};
            Extent stride{};
            for (size_t i = 0; i < count; ++i) {
                const BufferTextureCopy &region = regions[i];
                uint32_t mipLevel = region.texSubres.mipLevel;

                offset.x = region.texOffset.x == 0 ? 0 : utils::alignTo(region.texOffset.x, static_cast<int32_t>(blockSize.first));
                offset.y = region.texOffset.y == 0 ? 0 : utils::alignTo(region.texOffset.y, static_cast<int32_t>(blockSize.second));
                extent.width = region.texExtent.width < blockSize.first ? region.texExtent.width : utils::alignTo(region.texExtent.width, static_cast<uint32_t>(blockSize.first));
                extent.height = region.texExtent.height < blockSize.second ? region.texExtent.height : utils::alignTo(region.texExtent.height, static_cast<uint32_t>(blockSize.second));
                stride.width = region.buffStride > 0 ? region.buffStride : extent.width;
                stride.height = region.buffTexHeight > 0 ? region.buffTexHeight : extent.height;

                destWidth = (region.texExtent.width + offset.x == (gpuTexture->width >> mipLevel)) ? region.texExtent.width : extent.width;
                destHeight = (region.texExtent.height + offset.y == (gpuTexture->height >> mipLevel)) ? region.texExtent.height : extent.height;

                uint32_t faceCount = region.texSubres.baseArrayLayer + region.texSubres.layerCount;
                for (uint32_t f = region.texSubres.baseArrayLayer; f < faceCount; ++f) {
                    const uint8_t *buff;
                    if (stride.width == extent.width && stride.height == extent.height) {
                        buff = buffers[n++] + region.buffOffset;
                    } else {
                        buff = funcGLES2PixelBufferPick(buffers[n++], gpuTexture->format, region.buffOffset, stride, extent);
                    }

                    if (isCompressed) {
                        auto memSize = static_cast<GLsizei>(formatSize(gpuTexture->format, extent.width, extent.height, 1));
                        GL_CHECK(glCompressedTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f,
                                                           mipLevel,
                                                           offset.x, offset.y,
                                                           destWidth, destHeight,
                                                           gpuTexture->glFormat,
                                                           memSize,
                                                           (GLvoid *)buff));
                    } else {
                        GL_CHECK(glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f,
                                                 mipLevel,
                                                 offset.x, offset.y,
                                                 destWidth, destHeight,
                                                 gpuTexture->glFormat,
                                                 gpuTexture->glType,
                                                 (GLvoid *)buff));
                    }
                }
            }
            break;
        }
        default:
            CC_ABORT();
            break;
    }

    if (!isCompressed && hasFlag(gpuTexture->flags, TextureFlagBit::GEN_MIPMAP)) {
        GL_CHECK(glBindTexture(gpuTexture->glTarget, gpuTexture->glTexture));
        GL_CHECK(glGenerateMipmap(gpuTexture->glTarget));
    }
}

void cmdFuncGLES2CopyTextureToBuffers(GLES2Device *device, GLES2GPUTexture *gpuTexture, uint8_t *const *buffers, const BufferTextureCopy *regions, uint32_t count) {
    GLuint framebuffer = device->framebufferCacheMap()->getFramebufferFromTexture(gpuTexture);
    auto glFormat = gpuTexture->glFormat;
    auto glType = gpuTexture->glType;

    if (device->stateCache()->glFramebuffer != framebuffer) {
        GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, framebuffer));
        device->stateCache()->glFramebuffer = framebuffer;
    }

    for (uint32_t i = 0; i < count; ++i) {
        auto region = regions[i];
        uint8_t *copyDst = buffers[i];
        GL_CHECK(glReadPixels(region.texOffset.x, region.texOffset.y, region.texExtent.width, region.texExtent.height, glFormat, glType, copyDst));
    }
}

void cmdFuncGLES2CopyTexture(GLES2Device *device, GLES2GPUTexture *gpuTextureSrc, GLES2GPUTexture *gpuTextureDst, const TextureCopy *regions, uint32_t count) {
}

void cmdFuncGLES2BlitTexture(GLES2Device *device, GLES2GPUTexture *gpuTextureSrc, GLES2GPUTexture *gpuTextureDst, const TextureBlit *regions, uint32_t count, Filter filter) {
    GLES2GPUStateCache *cache = device->stateCache();

    GLuint dstFramebuffer = 0U;
    if (gpuTextureDst->swapchain) {
        dstFramebuffer = gpuTextureDst->swapchain->glFramebuffer;
    } else {
        dstFramebuffer = device->framebufferCacheMap()->getFramebufferFromTexture(gpuTextureDst);
    }
    device->context()->makeCurrent(gpuTextureDst->swapchain, gpuTextureSrc->swapchain);

    if (cache->glFramebuffer != dstFramebuffer) {
        GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, dstFramebuffer));
        cache->glFramebuffer = dstFramebuffer;
    }

    device->blitManager()->draw(gpuTextureSrc, gpuTextureDst, regions, count, filter);
}

void cmdFuncGLES2ExecuteCmds(GLES2Device *device, GLES2CmdPackage *cmdPackage) {
    if (!cmdPackage->cmds.size()) return;

    static uint32_t cmdIndices[static_cast<int>(GLESCmdType::COUNT)] = {0};
    memset(cmdIndices, 0, sizeof(cmdIndices));

    for (uint32_t i = 0; i < cmdPackage->cmds.size(); ++i) {
        GLESCmdType cmdType = cmdPackage->cmds[i];
        uint32_t &cmdIdx = cmdIndices[static_cast<int>(cmdType)];

        switch (cmdType) {
            case GLESCmdType::BEGIN_RENDER_PASS: {
                GLES2CmdBeginRenderPass *cmd = cmdPackage->beginRenderPassCmds[cmdIdx];
                cmdFuncGLES2BeginRenderPass(device, cmd->subpassIdx, cmd->gpuRenderPass, cmd->gpuFBO, &cmd->renderArea, cmd->clearColors, cmd->clearDepth, cmd->clearStencil);
                break;
            }
            case GLESCmdType::END_RENDER_PASS: {
                cmdFuncGLES2EndRenderPass(device);
                break;
            }
            case GLESCmdType::BIND_STATES: {
                GLES2CmdBindStates *cmd = cmdPackage->bindStatesCmds[cmdIdx];
                cmdFuncGLES2BindState(device, cmd->gpuPipelineState, cmd->gpuInputAssembler, cmd->gpuDescriptorSets.data(), cmd->dynamicOffsets.data(), &cmd->dynamicStates);
                break;
            }
            case GLESCmdType::DRAW: {
                GLES2CmdDraw *cmd = cmdPackage->drawCmds[cmdIdx];
                cmdFuncGLES2Draw(device, cmd->drawInfo);
                break;
            }
            case GLESCmdType::UPDATE_BUFFER: {
                GLES2CmdUpdateBuffer *cmd = cmdPackage->updateBufferCmds[cmdIdx];
                cmdFuncGLES2UpdateBuffer(device, cmd->gpuBuffer, cmd->buffer, cmd->offset, cmd->size);
                break;
            }
            case GLESCmdType::COPY_BUFFER_TO_TEXTURE: {
                GLES2CmdCopyBufferToTexture *cmd = cmdPackage->copyBufferToTextureCmds[cmdIdx];
                cmdFuncGLES2CopyBuffersToTexture(device, cmd->buffers, cmd->gpuTexture, cmd->regions, cmd->count);
                break;
            }
            case GLESCmdType::BLIT_TEXTURE: {
                GLES2CmdBlitTexture *cmd = cmdPackage->blitTextureCmds[cmdIdx];
                cmdFuncGLES2BlitTexture(device, cmd->gpuTextureSrc, cmd->gpuTextureDst, cmd->regions, cmd->count, cmd->filter);
                break;
            }
            default:
                break;
        }
        cmdIdx++;
    }
}

GLint cmdFuncGLES2GetMaxSampleCount() {
    GLint maxSamples = 1;
    GL_CHECK(glGetIntegerv(GL_MAX_SAMPLES_EXT, &maxSamples));
    return maxSamples;
}

void cmdFuncGLES2InsertMarker(GLES2Device *device, GLsizei length, const char *marker) {
    if (device->constantRegistry()->debugMarker) {
        glInsertEventMarkerEXT(length, marker);
    }
}

void cmdFuncGLES2PushGroupMarker(GLES2Device *device, GLsizei length, const char *marker) {
    if (device->constantRegistry()->debugMarker) {
        glPushGroupMarkerEXT(length, marker);
    }
}

void cmdFuncGLES2PopGroupMarker(GLES2Device *device) {
    if (device->constantRegistry()->debugMarker) {
        glPopGroupMarkerEXT();
    }
}

void GLES2GPUBlitManager::initialize() {
    _gpuShader.name = "Blit Pass";
    _gpuShader.blocks.push_back({
        0,
        0,
        "BlitParams",
        {
            {"tilingOffsetSrc", Type::FLOAT4, 1},
            {"tilingOffsetDst", Type::FLOAT4, 1},
        },
        1,
    });
    _gpuShader.samplerTextures.push_back({0, 1, "textureSrc", Type::SAMPLER2D, 1});
    _gpuShader.gpuStages.push_back({ShaderStageFlagBit::VERTEX, R"(
        precision mediump float;

        attribute vec2 a_position;
        attribute vec2 a_texCoord;

        uniform vec4 tilingOffsetSrc;
        uniform vec4 tilingOffsetDst;

        varying vec2 v_texCoord;

        void main() {
            v_texCoord = a_texCoord * tilingOffsetSrc.xy + tilingOffsetSrc.zw;
            gl_Position = vec4((a_position + 1.0) * tilingOffsetDst.xy - 1.0 + tilingOffsetDst.zw * 2.0, 0, 1);
        }
    )"});
    _gpuShader.gpuStages.push_back({ShaderStageFlagBit::FRAGMENT, R"(
        precision mediump float;

        uniform sampler2D textureSrc;

        varying vec2 v_texCoord;

        void main() {
            gl_FragColor = texture2D(textureSrc, v_texCoord);
        }
    )"});
    cmdFuncGLES2CreateShader(GLES2Device::getInstance(), &_gpuShader);

    _gpuDescriptorSetLayout.descriptorCount = 2;
    _gpuDescriptorSetLayout.bindingIndices = {0, 1};
    _gpuDescriptorSetLayout.descriptorIndices = {0, 1};
    _gpuDescriptorSetLayout.bindings.push_back({0, DescriptorType::UNIFORM_BUFFER, 1, ShaderStageFlagBit::VERTEX});
    _gpuDescriptorSetLayout.bindings.push_back({1, DescriptorType::SAMPLER_TEXTURE, 1, ShaderStageFlagBit::FRAGMENT});

    _gpuPipelineLayout.setLayouts.push_back(&_gpuDescriptorSetLayout);
    _gpuPipelineLayout.dynamicOffsetIndices.emplace_back();
    _gpuPipelineLayout.dynamicOffsetOffsets.push_back(0);

    _gpuPipelineState.glPrimitive = GL_TRIANGLE_STRIP;
    _gpuPipelineState.gpuShader = &_gpuShader;
    _gpuPipelineState.dss.depthTest = false;
    _gpuPipelineState.dss.depthWrite = false;
    _gpuPipelineState.gpuPipelineLayout = &_gpuPipelineLayout;

    _gpuVertexBuffer.usage = BufferUsage::VERTEX;
    _gpuVertexBuffer.memUsage = MemoryUsage::DEVICE;
    _gpuVertexBuffer.size = 16 * sizeof(float);
    _gpuVertexBuffer.stride = 4 * sizeof(float);
    _gpuVertexBuffer.count = 4;
    cmdFuncGLES2CreateBuffer(GLES2Device::getInstance(), &_gpuVertexBuffer);
    float data[]{
        -1.F, -1.F, 0.F, 0.F,
        1.F, -1.F, 1.F, 0.F,
        -1.F, 1.F, 0.F, 1.F,
        1.F, 1.F, 1.F, 1.F};
    cmdFuncGLES2UpdateBuffer(GLES2Device::getInstance(), &_gpuVertexBuffer, data, 0, sizeof(data));

    _gpuInputAssembler.attributes.push_back({"a_position", Format::RG32F});
    _gpuInputAssembler.attributes.push_back({"a_texCoord", Format::RG32F});
    _gpuInputAssembler.gpuVertexBuffers.push_back(&_gpuVertexBuffer);
    cmdFuncGLES2CreateInputAssembler(GLES2Device::getInstance(), &_gpuInputAssembler);

    _gpuPointSampler.minFilter = Filter::POINT;
    _gpuPointSampler.magFilter = Filter::POINT;
    cmdFuncGLES2CreateSampler(GLES2Device::getInstance(), &_gpuPointSampler);

    _gpuLinearSampler.minFilter = Filter::LINEAR;
    _gpuLinearSampler.magFilter = Filter::LINEAR;
    cmdFuncGLES2CreateSampler(GLES2Device::getInstance(), &_gpuLinearSampler);

    _gpuUniformBuffer.usage = BufferUsage::UNIFORM;
    _gpuUniformBuffer.memUsage = MemoryUsage::DEVICE | MemoryUsage::HOST;
    _gpuUniformBuffer.size = 8 * sizeof(float);
    _gpuUniformBuffer.stride = 8 * sizeof(float);
    _gpuUniformBuffer.count = 1;
    cmdFuncGLES2CreateBuffer(GLES2Device::getInstance(), &_gpuUniformBuffer);

    _gpuDescriptorSet.gpuDescriptors.push_back({DescriptorType::UNIFORM_BUFFER, &_gpuUniformBuffer});
    _gpuDescriptorSet.gpuDescriptors.push_back({DescriptorType::SAMPLER_TEXTURE});
    _gpuDescriptorSet.descriptorIndices = &_gpuDescriptorSetLayout.descriptorIndices;

    _drawInfo.vertexCount = 4;
}

void GLES2GPUBlitManager::destroy() {
    GLES2Device *device = GLES2Device::getInstance();
    cmdFuncGLES2DestroyBuffer(device, &_gpuVertexBuffer);
    cmdFuncGLES2DestroyInputAssembler(device, &_gpuInputAssembler);
    cmdFuncGLES2DestroySampler(device, &_gpuPointSampler);
    cmdFuncGLES2DestroySampler(device, &_gpuLinearSampler);
    cmdFuncGLES2DestroyBuffer(device, &_gpuVertexBuffer);
    cmdFuncGLES2DestroyShader(device, &_gpuShader);
}

static void ensureScissorRect(GLES2GPUStateCache *cache, int32_t x, int32_t y, uint32_t width, uint32_t height) {
    if (cache->viewport.left > x ||
        cache->viewport.top > y ||
        cache->viewport.width < width ||
        cache->viewport.height < height) {
        cache->viewport.left = std::min(cache->viewport.left, x);
        cache->viewport.top = std::min(cache->viewport.top, y);
        cache->viewport.width = std::max(cache->viewport.width, width);
        cache->viewport.height = std::max(cache->viewport.height, height);
        GL_CHECK(glViewport(cache->viewport.left, cache->viewport.top, cache->viewport.width, cache->viewport.height));
    }

    if (cache->scissor.x > x ||
        cache->scissor.y > y ||
        cache->scissor.width < width ||
        cache->scissor.height < height) {
        cache->scissor.x = std::min(cache->scissor.x, x);
        cache->scissor.y = std::min(cache->scissor.y, y);
        cache->scissor.width = std::max(cache->scissor.width, width);
        cache->scissor.height = std::max(cache->scissor.height, height);
        GL_CHECK(glScissor(cache->scissor.x, cache->scissor.y, cache->scissor.width, cache->scissor.height));
    }
}

void GLES2GPUBlitManager::draw(GLES2GPUTexture *gpuTextureSrc, GLES2GPUTexture *gpuTextureDst,
                               const TextureBlit *regions, uint32_t count, Filter filter) {
    GLES2GPUDescriptorSet *gpuDescriptorSet = &_gpuDescriptorSet;
    GLES2Device *device = GLES2Device::getInstance();
    auto &descriptor = _gpuDescriptorSet.gpuDescriptors.back();

    glViewport(0, 0, static_cast<int>(gpuTextureDst->width), static_cast<int>(gpuTextureDst->height));
    glScissor(0, 0, static_cast<int>(gpuTextureDst->width), static_cast<int>(gpuTextureDst->height));

    descriptor.gpuTexture = gpuTextureSrc;
    descriptor.gpuSampler = filter == Filter::POINT ? &_gpuPointSampler : &_gpuLinearSampler;
    for (uint32_t i = 0U; i < count; ++i) {
        const auto &region = regions[i];

        auto srcWidth = static_cast<float>(gpuTextureSrc->width);
        auto srcHeight = static_cast<float>(gpuTextureSrc->height);
        auto dstWidth = static_cast<float>(gpuTextureDst->width);
        auto dstHeight = static_cast<float>(gpuTextureDst->height);

        _uniformBuffer[0] = static_cast<float>(region.srcExtent.width) / srcWidth;
        _uniformBuffer[1] = static_cast<float>(region.srcExtent.height) / srcHeight;
        _uniformBuffer[2] = static_cast<float>(region.srcOffset.x) / srcWidth;
        _uniformBuffer[3] = static_cast<float>(region.srcOffset.y) / srcHeight;
        _uniformBuffer[4] = static_cast<float>(region.dstExtent.width) / dstWidth;
        _uniformBuffer[5] = static_cast<float>(region.dstExtent.height) / dstHeight;
        _uniformBuffer[6] = static_cast<float>(region.dstOffset.x) / dstWidth;
        _uniformBuffer[7] = static_cast<float>(region.dstOffset.y) / dstHeight;

        ensureScissorRect(device->stateCache(), region.dstOffset.x, region.dstOffset.y, region.dstExtent.width, region.dstExtent.height);

        cmdFuncGLES2UpdateBuffer(device, &_gpuUniformBuffer, _uniformBuffer, 0, sizeof(_uniformBuffer));
        cmdFuncGLES2BindState(device, &_gpuPipelineState, &_gpuInputAssembler, &gpuDescriptorSet);
        cmdFuncGLES2Draw(device, _drawInfo);
    }

    const auto &origViewport = device->stateCache()->viewport;
    const auto &origScissor = device->stateCache()->scissor;
    glViewport(origViewport.left, origViewport.top, static_cast<int>(origViewport.width), static_cast<int>(origViewport.height));
    glScissor(origScissor.x, origScissor.y, static_cast<int>(origScissor.width), static_cast<int>(origScissor.height));
}

void GLES2GPUFramebufferHub::update(GLES2GPUTexture *texture) {
    auto &pool = _framebuffers[texture];
    for (auto *framebuffer : pool) {
        cmdFuncGLES2DestroyFramebuffer(GLES2Device::getInstance(), framebuffer);
        cmdFuncGLES2CreateFramebuffer(GLES2Device::getInstance(), framebuffer);
    }
}

} // namespace gfx
} // namespace cc
