// Released under the MIT License. See LICENSE for details.

#if BA_ENABLE_OPENGL
#include "ballistica/base/graphics/gl/renderer_gl.h"

#include <iterator>
#include <sstream>

#include "ballistica/base/graphics/component/special_component.h"
#include "ballistica/base/graphics/gl/mesh/mesh_asset_data_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_dual_texture_full_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_object_split_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_simple_full_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_simple_split_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_smoke_full_gl.h"
#include "ballistica/base/graphics/gl/mesh/mesh_data_sprite_gl.h"
#include "ballistica/base/graphics/gl/program/program_blur_gl.h"
#include "ballistica/base/graphics/gl/program/program_gl.h"
#include "ballistica/base/graphics/gl/program/program_object_gl.h"
#include "ballistica/base/graphics/gl/program/program_post_process_gl.h"
#include "ballistica/base/graphics/gl/program/program_shield_gl.h"
#include "ballistica/base/graphics/gl/program/program_simple_gl.h"
#include "ballistica/base/graphics/gl/program/program_smoke_gl.h"
#include "ballistica/base/graphics/gl/program/program_sprite_gl.h"
#include "ballistica/base/graphics/gl/render_target_gl.h"
#include "ballistica/base/graphics/gl/texture_data_gl.h"
#include "ballistica/base/platform/base_platform.h"

// Turn this off to see how much blend overdraw is occurring.
#define BA_GL_ENABLE_BLEND 1

// Support legacy drawing purely for debugging (should migrate this to
// post-fixed pipeline).
#define BA_GL_ENABLE_DEBUG_DRAW_COMMANDS 0

#ifndef GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
#define GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG 0x8C02
#endif
#ifndef GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG
#define GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG 0x8C03
#endif

#ifndef GL_ETC1_RGB8_OES
#define GL_ETC1_RGB8_OES 0x8D64
#endif

#ifndef GL_COMPRESSED_RGB8_ETC2
#define GL_COMPRESSED_RGB8_ETC2 0x9274
#endif
#ifndef GL_COMPRESSED_RGBA8_ETC2_EAC
#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278
#endif

namespace ballistica::base {

bool RendererGL::funky_depth_issue_set_{};
bool RendererGL::funky_depth_issue_{};

RendererGL::RendererGL() {
  assert(g_base->app_adapter->InGraphicsContext());

  if (explicit_bool(BA_FORCE_CHECK_GL_ERRORS)) {
    ScreenMessage("GL ERROR CHECKS ENABLED");
  }

  // Run any one-time setup the platform might need to do
  // (grabbing function pointers, etc.)
  if (!g_sys_gl_inited) {
    SysGLInit(this);
    g_sys_gl_inited = true;
  }

  CheckGLCapabilities_();
  SyncGLState_();
}

void RendererGL::CheckGLError(const char* file, int line) {
  GLenum err = glGetError();
  if (err != GL_NO_ERROR) {
    const char* version = (const char*)glGetString(GL_VERSION);
    BA_PRECONDITION_FATAL(version);
    const char* vendor = (const char*)glGetString(GL_VENDOR);
    BA_PRECONDITION_FATAL(vendor);
    const char* renderer = (const char*)glGetString(GL_RENDERER);
    BA_PRECONDITION_FATAL(renderer);
    Log(LogLevel::kError,
        "OpenGL Error at " + std::string(file) + " line " + std::to_string(line)
            + ": " + GLErrorToString(err) + "\nrenderer: " + renderer
            + "\nvendor: " + vendor + "\nversion: " + version
            + "\ntime: " + std::to_string(g_core->GetAppTimeMillisecs()));
  }
}

auto RendererGL::GLErrorToString(GLenum err) -> std::string {
  switch (err) {
    case GL_NO_ERROR:
      return "GL_NO_ERROR";
    case GL_INVALID_ENUM:
      return "GL_INVALID_ENUM";
    case GL_INVALID_VALUE:
      return "GL_INVALID_VALUE";
    case GL_INVALID_OPERATION:
      return "GL_INVALID_OPERATION";
    case GL_OUT_OF_MEMORY:
      return "GL_OUT_OF_MEMORY";
    case GL_INVALID_FRAMEBUFFER_OPERATION:
      return "GL_INVALID_FRAMEBUFFER_OPERATION";
    default:
      return std::to_string(err);
  }
}

// Look for a gl extension prefixed by "GL_ARB", "GL_EXT", etc. Returns true
// if found.
static auto CheckGLExtension(const std::vector<std::string>& exts,
                             const char* ext) -> bool {
  assert(strlen(ext) < 100);
  const int variant_count{10};
  char variants[variant_count][128];
  int i = 0;
  snprintf(variants[i], sizeof(variants[i]), "OES_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_OES_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_KHR_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_ARB_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_APPLE_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_EXT_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_NV_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_ATI_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_SGIS_%s", ext);
  i++;
  snprintf(variants[i], sizeof(variants[i]), "GL_IMG_%s", ext);
  i++;
  assert(i == variant_count);

  for (auto&& ext : exts) {
    for (int i = 0; i < variant_count; ++i) {
      if (variants[i] == ext) {
        return true;
      }
    }
  }
  return false;
}

// This is split into its own call because systems that load GL calls
// dynamically may want to run the check before trying to load said GL
// calls. It's better to die with a 'Your OpenGL is too old' error rather
// than a 'Could not load function foofDinglePlop2XZ'.
void RendererGL::CheckGLVersion() {
  if (checked_gl_version_) {
    return;
  }
  const char* version_str = (const char*)glGetString(GL_VERSION);
  BA_PRECONDITION_FATAL(version_str);

  // Do a rough check to make sure we're running 3 or newer of GL/GLES. This
  // query should be available even on older versions which is why we do it
  // before the GL_MAJOR_VERSION/GL_MINOR_VERSION business which is not.
  if (gl_is_es()) {
    // GL ES version strings start with 'OpenGL ES X' with X being version.
    const char* prefix = "OpenGL ES ";
    int prefixlen = strlen(prefix);
    BA_PRECONDITION_FATAL(!strncmp(version_str, prefix, prefixlen));
    if (version_str[prefixlen] != '3') {
      FatalError(
          std::string("Your OpenGL ES version is too old (") + version_str
          + "). We require 3.0 or later. Try updating your graphics drivers.");
    }
  } else {
    // Regular GL version strings start with numeric version.

    if (version_str[0] != '3' && version_str[0] != '4') {
      FatalError(
          std::string("Your OpenGL version is too old (") + version_str
          + "). We require 3.0 or later. Try updating your graphics drivers.");
    }
  }
  checked_gl_version_ = true;
}

void RendererGL::CheckGLCapabilities_() {
  BA_DEBUG_CHECK_GL_ERROR;
  assert(g_base->app_adapter->InGraphicsContext());

  // Die if our overall GL version is too old.
  CheckGLVersion();

  const char* renderer = (const char*)glGetString(GL_RENDERER);
  BA_PRECONDITION_FATAL(renderer);
  const char* vendor = (const char*)glGetString(GL_VENDOR);
  BA_PRECONDITION_FATAL(vendor);
  const char* version_str = (const char*)glGetString(GL_VERSION);
  BA_PRECONDITION_FATAL(version_str);

  // Now fetch exact major/minor versions. This query requires version 3.0
  // or newer which is why we checked overall version in CheckGLVersion()
  // above.
  glGetError();  // Clear any existing error so we don't die on it here.
  glGetIntegerv(GL_MAJOR_VERSION, &gl_version_major_);
  BA_PRECONDITION_FATAL(glGetError() == GL_NO_ERROR);
  glGetIntegerv(GL_MINOR_VERSION, &gl_version_minor_);
  BA_PRECONDITION_FATAL(glGetError() == GL_NO_ERROR);

  const char* basestr;
  if (gl_is_es()) {
    basestr = "OpenGL ES";
  } else {
    basestr = "OpenGL";
  }

  if (g_buildconfig.debug_build()) {
    Log(LogLevel::kInfo, std::string("Using ") + basestr + " (vendor: " + vendor
                             + ", renderer: " + renderer
                             + ", version: " + version_str + ").");
  }

  // Build a vector of extensions. Newer GLs give us extensions as lists
  // already, but on older ones we may need to break a single string apart
  // ourself.
  std::vector<std::string> extensions;
  bool used_num_extensions{};

  // Do the modern gl thing of looking through a list of extensions; not a
  // single string.
  if (auto num_extensions = GLGetIntOptional(GL_NUM_EXTENSIONS)) {
    used_num_extensions = true;
    extensions.reserve(*num_extensions);
    for (int i = 0; i < num_extensions; ++i) {
      const char* extension = (const char*)glGetStringi(GL_EXTENSIONS, i);
      BA_PRECONDITION(extension);
      extensions.push_back(extension);
    }
  } else {
    Log(LogLevel::kWarning, "Falling back on legacy GL_EXTENSIONS parsing.");
    // Fall back on parsing the single giant string if need be.
    // (Can probably kill this).
    auto* ex = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
    BA_DEBUG_CHECK_GL_ERROR;
    BA_PRECONDITION_FATAL(ex);
    std::istringstream iss(ex);
    extensions = {std::istream_iterator<std::string>(iss),
                  std::istream_iterator<std::string>()};
  }

  // On Android, look at the GL version and try to get gl3 funcs to
  // determine if we're running ES3 or not.
#if BA_OSTYPE_ANDROID
  //   bool have_es3;
  // #if BA_USE_ES3_INCLUDES
  //   have_es3 = true;
  // #else
  //   have_es3 = (strstr(version_str, "OpenGL ES 3.") && gl3stubInit());
  // #endif  // BA_OSTYPE_ANDROID

  //   if (have_es3) {
  //     g_running_es3 = true;
  //   } else {
  // #if !BA_USE_ES3_INCLUDES
  //     g_running_es3 = false;

  //     // Can still support some stuff like framebuffer-blit with es2
  //     extensions. assert(glBlitFramebuffer == nullptr ||
  //     !first_extension_check_); glBlitFramebuffer =
  //         (decltype(glBlitFramebuffer))eglGetProcAddress("glBlitFramebufferNV");
  //     assert(glRenderbufferStorageMultisample == nullptr
  //            || !first_extension_check_);
  //     glRenderbufferStorageMultisample =
  //         (decltype(glRenderbufferStorageMultisample))eglGetProcAddress(
  //             "glRenderbufferStorageMultisampleNV");

  //     assert(glGenVertexArrays == nullptr || !first_extension_check_);
  //     glGenVertexArrays =
  //         (decltype(glGenVertexArrays))eglGetProcAddress("glGenVertexArraysOES");
  //     assert(glDeleteVertexArrays == nullptr || !first_extension_check_);
  //     glDeleteVertexArrays =
  //     (decltype(glDeleteVertexArrays))eglGetProcAddress(
  //         "glDeleteVertexArraysOES");
  //     assert(glBindVertexArray == nullptr || !first_extension_check_);
  //     glBindVertexArray =
  //         (decltype(glBindVertexArray))eglGetProcAddress("glBindVertexArrayOES");

  // #endif  // BA_USE_ES3_INCLUDES
  //   }

  BA_DEBUG_CHECK_GL_ERROR;

  // Flag certain devices as 'speedy' - we use this to enable high/higher
  // quality and whatnot (even in cases where ES3 isnt available).

  // Let just consider ES 3.2 stuff speedy.
  assert(gl_version_major() == 3);
  is_speedy_android_device_ = gl_version_minor() >= 2;

  // is_extra_speedy_android_device_ = false;
  is_adreno_ = (strstr(renderer, "Adreno") != nullptr);
  // draws_shields_funny_ = false;  // Start optimistic.

  // Ali tv box.
  // if (!strcmp(renderer, "Mali-450 MP")) {
  //   is_speedy_android_device_ = true;  // this is borderline
  //   speedy/extra-speedy
  //   // draws_shields_funny_ = true;
  // }

  // Firetv, etc.. lets enable MSAA.
  // if (!strcmp(renderer, "Adreno (TM) 320")) {
  //   is_recent_adreno_ = true;
  // }

  // This is right on the borderline, but lets go with extra-speedy I guess.
  // if (!strcmp(renderer, "Adreno (TM) 330")) {
  //   is_recent_adreno_ = true;
  //   is_extra_speedy_android_device_ = true;
  // }

  // *any* of the 4xx or 5xx series are extra-speedy.
  // if (strstr(renderer, "Adreno (TM) 4") || strstr(renderer, "Adreno (TM) 5")
  //     || strstr(renderer, "Adreno (TM) 6")) {
  //   is_extra_speedy_android_device_ = true;
  //   is_recent_adreno_ = true;
  // }

  // Some speedy malis (Galaxy S6 / Galaxy S7-ish).
  // if (strstr(renderer, "Mali-T760") || strstr(renderer, "Mali-T860")
  //     || strstr(renderer, "Mali-T880")) {
  //   is_extra_speedy_android_device_ = true;
  // }

  // Note 8 is speed-tastic
  // if (!strcmp(renderer, "Mali-G71") || !strcmp(renderer, "Mali-G72")) {
  //   is_extra_speedy_android_device_ = true;
  // }

  // Covers Nexus player.
  // HMM Scratch that - this winds up being too slow for phones using this chip.
  // if (strstr(renderer, "PowerVR Rogue G6430")) {
  // is_extra_speedy_android_device_ = true;
  // }

  // Figure out if we're a Tegra 4/K1/etc since we do some special stuff on
  // those...
  // if (!strcmp(renderer, "NVIDIA Tegra")) {
  //   // tegra 4 won't have ES3 but will have framebuffer_multisample
  //   if (!g_running_es3 && CheckGLExtension(ex, "framebuffer_multisample")) {
  //     is_tegra_4_ = true;
  //     is_speedy_android_device_ = true;
  //   } else if (g_running_es3) {
  //     // running ES3 - must be a K1 (for now)
  //     is_tegra_k1_ = true;
  //     is_extra_speedy_android_device_ = true;
  //   } else {
  //     // looks like Tegra-2 era stuff was just "NVIDIA Tegra" as well...
  //   }
  // }

  // Also store this globally for a few other bits of the app to use..
  // g_core->platform->set_is_tegra_k1(is_tegra_k1_);

  // Extra-speedy implies speedy too..
  // if (is_extra_speedy_android_device_) {
  //   is_speedy_android_device_ = true;
  // }

#endif  // BA_OSTYPE_ANDROID

  std::list<TextureCompressionType> c_types;
  assert(g_base->graphics);
  if (CheckGLExtension(extensions, "texture_compression_s3tc")) {
    c_types.push_back(TextureCompressionType::kS3TC);
  }

  // Limiting pvr support to iOS for the moment.
  if (!g_buildconfig.ostype_android()) {
    if (CheckGLExtension(extensions, "texture_compression_pvrtc")) {
      c_types.push_back(TextureCompressionType::kPVR);
    }
  }

  // Pretty much all Android devices should support ETC1.
  if (CheckGLExtension(extensions, "compressed_ETC1_RGB8_texture")) {
    c_types.push_back(TextureCompressionType::kETC1);
  } else {
    if (g_buildconfig.ostype_android()) {
      Log(LogLevel::kError, "Android device missing ETC1 support.");
    }
  }

  // ETC2 is required for ES3 support (and OpenGL 4.4 or something once we
  // eventually get there).
  if (gl_is_es()) {
    c_types.push_back(TextureCompressionType::kETC2);
  }

  // ASTC is generally available on newer mobile hardware.
  if (CheckGLExtension(extensions, "texture_compression_astc_ldr")) {
    c_types.push_back(TextureCompressionType::kASTC);
  }

  g_base->graphics_server->SetTextureCompressionTypes(c_types);

  // Both GL 3 and GL ES 3.0 support depth textures (and thus our high
  // quality mode) as a core feature.
  // g_base->graphics->SetSupportsHighQualityGraphics(true);

  // Store the tex-compression type we support.
  BA_DEBUG_CHECK_GL_ERROR;

  // Anisotropic sampling is still an extension as of both GL 3 and ES 3,
  // so we need to test for it.
  anisotropic_support_ =
      CheckGLExtension(extensions, "texture_filter_anisotropic");
  if (anisotropic_support_) {
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &max_anisotropy_);
  }

  BA_DEBUG_CHECK_GL_ERROR;

  if (gl_is_es()) {
    // GL ES 3 has glInvalidateFramebuffer as part of the standard.
    invalidate_framebuffer_support_ = true;
  } else {
    // It seems it's standard as of desktop GL 4.3 so we could probably
    // use it selectively if we wanted.
    invalidate_framebuffer_support_ = false;
  }

  combined_texture_image_unit_count_ =
      GLGetInt(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS);

  // If we're running ES3, ask about our max multisample counts and whether we
  // can enable MSAA.
  msaa_max_samples_rgb565_ = msaa_max_samples_rgb8_ = 0;  // start pessimistic

  bool have_gl_get_internal_format_iv{};
  if (gl_is_es()) {
    // This is available on ES 3.
    have_gl_get_internal_format_iv = true;
  } else {
    // This is available on GL 4.2 or newer.
    if (gl_version_major() == 4 && gl_version_minor() >= 2) {
      have_gl_get_internal_format_iv = true;
    }
  }

  if (have_gl_get_internal_format_iv) {
    GLint count;
    glGetInternalformativ(GL_RENDERBUFFER, GL_RGB565, GL_NUM_SAMPLE_COUNTS, 1,
                          &count);
    if (count > 0) {
      std::vector<GLint> samples;
      samples.resize(static_cast<size_t>(static_cast<size_t>(count)));
      glGetInternalformativ(GL_RENDERBUFFER, GL_RGB565, GL_SAMPLES, count,
                            &samples[0]);
      msaa_max_samples_rgb565_ = samples[0];
    } else {
      BA_LOG_ONCE(LogLevel::kError, "Got 0 samplecounts for RGB565");
      msaa_max_samples_rgb565_ = 0;
    }

    // RGB8 max multisamples.
    glGetInternalformativ(GL_RENDERBUFFER, GL_RGB8, GL_NUM_SAMPLE_COUNTS, 1,
                          &count);
    if (count > 0) {
      std::vector<GLint> samples;
      samples.resize(static_cast<size_t>(count));
      glGetInternalformativ(GL_RENDERBUFFER, GL_RGB8, GL_SAMPLES, count,
                            &samples[0]);
      msaa_max_samples_rgb8_ = samples[0];
    } else {
      BA_LOG_ONCE(LogLevel::kError, "Got 0 samplecounts for RGB8");
      msaa_max_samples_rgb8_ = 0;
    }
  } else {
    // For older GL (which includes all Macs) it sounds like this is the way
    // to query max samples?.. but I don't know for sure if this applies to
    // renderbuffer targets or just the default drawable. Will it ever be
    // different?
    auto max_samples = GLGetIntOptional(GL_MAX_SAMPLES);
    if (max_samples.has_value()) {
      msaa_max_samples_rgb565_ = msaa_max_samples_rgb8_ = *max_samples;
    }
  }

  BA_DEBUG_CHECK_GL_ERROR;

  first_extension_check_ = false;
}

auto RendererGL::GetMSAASamplesForFramebuffer_(int width, int height) -> int {
  if (g_buildconfig.ostype_android()) {
    // We currently aim for 4 up to 800 height and 2 beyond that.
    if (height > 800) {
      return 2;
    } else {
      return 4;
    }
  } else {
    return 4;
  }
}

void RendererGL::UpdateMSAAEnabled_() {
  if (g_buildconfig.ostype_macos()) {
    // Let's go ahead and flip this on for Apple Silicon Macs.
#if __aarch64__
    enable_msaa_ = true;
#else
    enable_msaa_ = false;
#endif
  } else if (g_buildconfig.rift_build()) {
    if (msaa_max_samples_rgb8_ > 0) {
      enable_msaa_ = true;
    } else {
      enable_msaa_ = false;
    }
  } else if (g_buildconfig.ostype_android()) {
    // lets allow full 1080p msaa with newer stuff..
    int max_msaa_res = is_tegra_k1_ ? 1200 : 800;

    // To start, see if it looks like we support msaa on paper.
    enable_msaa_ =
        ((screen_render_target()->physical_height()
          <= static_cast<float>(max_msaa_res))
         && (msaa_max_samples_rgb8_ > 0) && (msaa_max_samples_rgb565_ > 0));

    // Ok, lets be careful here; msaa blitting/etc seems to be particular in
    // terms of supported formats/etc so let's only enable it on
    // explicitly-tested hardware for now.
    if (!is_tegra_4_ && !is_tegra_k1_ && !is_recent_adreno_) {
      enable_msaa_ = false;
    }
  } else {
    enable_msaa_ = false;
  }
}

auto RendererGL::IsMSAAEnabled() const -> bool { return enable_msaa_; }

auto RendererGL::GetGLTextureFormat(TextureFormat f) -> GLenum {
  switch (f) {
    case TextureFormat::kDXT1:
      return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
      break;
    case TextureFormat::kDXT5:
      return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
      break;
    case TextureFormat::kPVR2:
      return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
      break;
    case TextureFormat::kPVR4:
      return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
      break;
    case TextureFormat::kETC1:
      return GL_ETC1_RGB8_OES;
      break;
    case TextureFormat::kETC2_RGB:
      return GL_COMPRESSED_RGB8_ETC2;
      break;
    case TextureFormat::kETC2_RGBA:
      return GL_COMPRESSED_RGBA8_ETC2_EAC;
      break;
    default:
      throw Exception("Invalid TextureFormat: "
                      + std::to_string(static_cast<int>(f)));
  }
}

void RendererGL::SetViewport_(GLint x, GLint y, GLsizei width, GLsizei height) {
  if (x != viewport_x_ || y != viewport_y_ || width != viewport_width_
      || height != viewport_height_) {
    viewport_x_ = x;
    viewport_y_ = y;
    viewport_width_ = width;
    viewport_height_ = height;
    glViewport(viewport_x_, viewport_y_, viewport_width_, viewport_height_);
  }
}

void RendererGL::BindTextureUnit(uint32_t tex_unit) {
  assert(tex_unit >= 0 && tex_unit < kMaxGLTexUnitsUsed);
  if (active_tex_unit_ != -1) {
    // Make sure our internal state stays correct.
    assert(GLGetInt(GL_ACTIVE_TEXTURE) == GL_TEXTURE0 + active_tex_unit_);
  }
  if (active_tex_unit_ != tex_unit) {
    active_tex_unit_ = tex_unit;
    glActiveTexture(GL_TEXTURE0 + active_tex_unit_);
    BA_DEBUG_CHECK_GL_ERROR;
  } else {
  }
}

auto RendererGL::GLGetInt(GLenum name) -> int {
  assert(g_base->app_adapter->InGraphicsContext());

  // Clear any error coming in; don't want to fail for something that's not
  // our problem.
  if (g_buildconfig.debug_build()) {
    BA_DEBUG_CHECK_GL_ERROR;
  } else {
    glGetError();
  }
  GLint val;
  glGetIntegerv(name, &val);
  if (glGetError() != GL_NO_ERROR) {
    FatalError("Unable to fetch GL int " + std::to_string(name));
  }
  return val;
}

auto RendererGL::GLGetIntOptional(GLenum name) -> std::optional<int> {
  assert(g_base->app_adapter->InGraphicsContext());

  // Clear any error coming in; don't want to fail for something that's not
  // our problem.
  if (g_buildconfig.debug_build()) {
    BA_DEBUG_CHECK_GL_ERROR;
  } else {
    glGetError();
  }
  GLint val;
  glGetIntegerv(name, &val);
  if (glGetError() != GL_NO_ERROR) {
    return {};
  }
  return val;
}

void RendererGL::BindFramebuffer(GLuint fb) {
  if (active_framebuffer_ != fb) {
    glBindFramebuffer(GL_FRAMEBUFFER, fb);
    active_framebuffer_ = fb;
  } else {
    assert(GLGetInt(GL_FRAMEBUFFER_BINDING) == fb);
  }
}

void RendererGL::BindArrayBuffer(GLuint b) {
  if (active_array_buffer_ != -1) {
    // Make sure our internal state stays correct.
    assert(GLGetInt(GL_ARRAY_BUFFER_BINDING) == active_array_buffer_);
  }
  if (active_array_buffer_ != b) {
    glBindBuffer(GL_ARRAY_BUFFER, b);
    active_array_buffer_ = b;
  }
}

void RendererGL::BindTexture_(GLuint type, const TextureAsset* t,
                              GLuint tex_unit) {
  if (t) {
    auto data = static_cast_check_type<TextureDataGL*>(t->renderer_data());
    BindTexture_(type, data->GetTexture(), tex_unit);
  } else {
    // Fallback to noise.
    BindTexture_(type, random_tex_, tex_unit);
  }
}

void RendererGL::BindTexture_(GLuint type, GLuint tex, GLuint tex_unit) {
  switch (type) {
    case GL_TEXTURE_2D: {
      // Make sure our internal state stays correct.
      if (g_buildconfig.debug_build()) {
        if (bound_textures_2d_[tex_unit] != -1) {
          BindTextureUnit(tex_unit);
          assert(GLGetInt(GL_TEXTURE_BINDING_2D)
                 == bound_textures_2d_[tex_unit]);
        }
      }
      if (tex != bound_textures_2d_[tex_unit]) {
        BindTextureUnit(tex_unit);
        glBindTexture(type, tex);
        bound_textures_2d_[tex_unit] = tex;
      }
      break;
    }
    case GL_TEXTURE_CUBE_MAP: {
      // Make sure our internal state stays correct.
      if (g_buildconfig.debug_build()) {
        if (bound_textures_cube_map_[tex_unit] != -1) {
          BindTextureUnit(tex_unit);
          assert(GLGetInt(GL_TEXTURE_BINDING_CUBE_MAP)
                 == bound_textures_cube_map_[tex_unit]);
        }
      }
      if (tex != bound_textures_cube_map_[tex_unit]) {
        BindTextureUnit(tex_unit);
        glBindTexture(type, tex);
        bound_textures_cube_map_[tex_unit] = tex;
      }
      break;
    }
    default:
      throw Exception();
  }
}

void RendererGL::CheckFunkyDepthIssue_() {
  if (funky_depth_issue_set_) {
    return;
  }

  // Note: this test fails for some reason on some Broadcom VideoCore and
  // older NVidia chips (tegra 2?) ...so lets limit testing to adreno chips
  // since that's the only place the problem is known to happen.
  if (!is_adreno_) {
    // if (!is_adreno_ || !supports_depth_textures_) {
    funky_depth_issue_set_ = true;
    funky_depth_issue_ = false;
    return;
  }

  // On some adreno chips, depth buffer values are always returned
  // in a 0-1 range in shaders even if a depth range is set; everywhere
  // else they return that depth range.
  // To test for this, we can create a temp buffer, clear it, set a depth range,

  Object::Ref<RenderTargetGL> test_rt1;
  Object::Ref<RenderTargetGL> test_rt2;

  test_rt1 = Object::New<RenderTargetGL>(this, 32, 32, true, true, true, true,
                                         false, false, false);
  BA_DEBUG_CHECK_GL_ERROR;
  test_rt2 = Object::New<RenderTargetGL>(this, 32, 32, true, false, true, false,
                                         false, false, false);
  BA_DEBUG_CHECK_GL_ERROR;

  // This screws up some qualcomm chips.
  SetDepthRange(0.0f, 0.5f);

  // Draw a flat color plane into our first render target.
  SetDepthWriting(true);
  SetDepthTesting(true);
  SetBlend(false);
  SetDoubleSided_(false);
  test_rt1->DrawBegin(true, 1.0f, 1.0f, 1.0f, 1.0f);
  ProgramSimpleGL* p = simple_color_prog_;
  p->Bind();
  p->SetColor(1, 0, 1);
  g_base->graphics_server->ModelViewReset();
  g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1);
  GetActiveProgram_()->PrepareToDraw();
  screen_mesh_->Bind();
  screen_mesh_->Draw(DrawType::kTriangles);
  BA_DEBUG_CHECK_GL_ERROR;

  // Now draw into a second buffer the difference between the depth tex
  // lookup and the gl frag depth.
  SetDepthWriting(false);
  SetDepthTesting(false);
  SetBlend(false);
  SetDoubleSided_(false);
  test_rt2->DrawBegin(false, 1.0f, 1.0f, 1.0f, 1.0f);
  p = simple_tex_dtest_prog_;
  p->Bind();
  g_base->graphics_server->ModelViewReset();
  g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1);
  p->SetColorTexture(test_rt1->framebuffer()->depth_texture());
  GetActiveProgram_()->PrepareToDraw();
  screen_mesh_->Bind();
  screen_mesh_->Draw(DrawType::kTriangles);
  BA_DEBUG_CHECK_GL_ERROR;

  // Now sample a pixel from our render-target. If the depths matched, the
  // value will be 0; otherwise it'll be 30 or so (allow a bit of leeway to
  // account for dithering/etc.).
  uint8_t buffer[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
  glReadPixels(0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, buffer);

  // Sample 4 pixels to reduce effects of dithering.
  funky_depth_issue_ =
      ((buffer[0] + buffer[4] + buffer[8] + buffer[12]) / 4 >= 15);
  funky_depth_issue_set_ = true;

  BA_DEBUG_CHECK_GL_ERROR;
}

void RendererGL::PushGroupMarker(const char* label) {
  BA_GL_PUSH_GROUP_MARKER(label);
}
void RendererGL::PopGroupMarker() { BA_GL_POP_GROUP_MARKER(); }

void RendererGL::InvalidateFramebuffer(bool color, bool depth,
                                       bool target_read_framebuffer) {
  BA_DEBUG_CHECK_GL_ERROR;

  // Currently this is ES only for us.
#if BA_OPENGL_IS_ES

  if (invalidate_framebuffer_support()) {
    GLenum attachments[5];
    // Need to use different flags for the main framebuffer.
    int count = 0;
    if (active_framebuffer_ == 0 && !target_read_framebuffer) {
      if (color) {
        attachments[count++] = GL_COLOR;
      }
      if (depth) {
        attachments[count++] = GL_DEPTH;
      }
    } else {
      if (color) {
        attachments[count++] = GL_COLOR_ATTACHMENT0;
      }
      if (depth) {
        attachments[count++] = GL_DEPTH_ATTACHMENT;
      }

      glInvalidateFramebuffer(
          target_read_framebuffer ? GL_READ_FRAMEBUFFER : GL_FRAMEBUFFER, count,
          attachments);
    }
    BA_DEBUG_CHECK_GL_ERROR;
  }
#else
  // Make noise if we should be doing this here too at some point.
  assert(!invalidate_framebuffer_support());
#endif  // BA_OPENGL_IS_ES
}

RendererGL::~RendererGL() {
  assert(g_base->app_adapter->InGraphicsContext());
  printf("FIXME: need to unload renderer on destroy.\n");
  // Unload();
  BA_DEBUG_CHECK_GL_ERROR;
}

void RendererGL::UseProgram_(ProgramGL* p) {
  if (p != current_program_) {
    glUseProgram(p->program());
    current_program_ = p;
  }
}

void RendererGL::SyncGLState_() {
  BA_DEBUG_CHECK_GL_ERROR;

#if BA_RIFT_BUILD
  if (g_core->vr_mode()) {
    glFrontFace(GL_CCW);
  }
  BA_DEBUG_CHECK_GL_ERROR;

  // if (time(nullptr)%2 == 0) {
  //   glEnable(GL_FRAMEBUFFER_SRGB);
  // } else {
  //   glDisable(GL_FRAMEBUFFER_SRGB);
  // }
#endif  // BA_RIFT_BUILD

  active_tex_unit_ = -1;      // force a set next time
  active_framebuffer_ = -1;   // ditto
  active_array_buffer_ = -1;  // ditto
  for (int i = 0; i < kMaxGLTexUnitsUsed; i++) {
    bound_textures_2d_[i] = -1;        // ditto
    bound_textures_cube_map_[i] = -1;  // ditto
  }
  glUseProgram(0);
  BA_DEBUG_CHECK_GL_ERROR;
  current_program_ = nullptr;
  current_vertex_array_ = 0;

  glBindVertexArray(0);
  BA_DEBUG_CHECK_GL_ERROR;

  // Wack these out so the next call will definitely call glViewport.
  viewport_x_ = -9999;
  viewport_y_ = -9999;
  viewport_width_ = -9999;
  viewport_height_ = -9999;

  glDisable(GL_BLEND);
  blend_ = false;

  // Currently we only ever write to an alpha buffer for our vr flat overlay
  // texture, and in that case we need alpha to accumulate; not get
  // overwritten. could probably enable this everywhere but I don't know if
  // it's supported on all hardware or slower.
  if (g_core->vr_mode()) {
#if BA_OSTYPE_WINDOWS
    if (glBlendFuncSeparate == nullptr) {
      FatalError(
          "VR mode is not supported by your GPU (no glBlendFuncSeparate); Try "
          "updating your drivers?...");
    }
#endif
    glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                        GL_ONE_MINUS_SRC_ALPHA);
    BA_DEBUG_CHECK_GL_ERROR;
  } else {
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    BA_DEBUG_CHECK_GL_ERROR;
  }
  blend_premult_ = false;
  glEnable(GL_CULL_FACE);
  glCullFace(GL_BACK);
  BA_DEBUG_CHECK_GL_ERROR;
  double_sided_ = false;
  draw_front_ = true;
  glDisable(GL_DEPTH_TEST);
  depth_testing_enabled_ = false;
  glDepthMask(static_cast<GLboolean>(true));
  depth_writing_enabled_ = true;
  draw_at_equal_depth_ = false;
  glDepthFunc(GL_LESS);
  depth_range_min_ = 0.0f;
  depth_range_max_ = 1.0f;
  glDepthRange(depth_range_min_, depth_range_max_);
  BA_DEBUG_CHECK_GL_ERROR;
}

#define GET_MESH_DATA(TYPE, VAR)                              \
  auto* VAR = static_cast<TYPE*>(mesh_data->renderer_data()); \
  assert(VAR&& VAR == dynamic_cast<TYPE*>(mesh_data->renderer_data()))

#define GET_INDEX_BUFFER()                                      \
  assert(buffer != buffers.end());                              \
  assert(index_size != index_sizes.end());                      \
  MeshIndexBuffer16* indices16{nullptr};                        \
  MeshIndexBuffer32* indices32{nullptr};                        \
  assert(*index_size == 4 || *index_size == 2);                 \
  bool use_indices32 = (*index_size == 4);                      \
  if (use_indices32) {                                          \
    indices32 = static_cast<MeshIndexBuffer32*>(buffer->Get()); \
    assert(indices32&& indices32                                \
           == dynamic_cast<MeshIndexBuffer32*>(buffer->Get())); \
  } else {                                                      \
    indices16 = static_cast<MeshIndexBuffer16*>(buffer->Get()); \
    assert(indices16&& indices16                                \
           == dynamic_cast<MeshIndexBuffer16*>(buffer->Get())); \
  }                                                             \
  index_size++;                                                 \
  buffer++

#define GET_BUFFER(TYPE, VAR)                              \
  assert(buffer != buffers.end());                         \
  auto* VAR = static_cast<TYPE*>(buffer->Get());           \
  assert(VAR&& VAR == dynamic_cast<TYPE*>(buffer->Get())); \
  buffer++

// Takes all latest mesh data from the client side and applies it
// to our gl implementations.
void RendererGL::UpdateMeshes(
    const std::vector<Object::Ref<MeshDataClientHandle> >& meshes,
    const std::vector<int8_t>& index_sizes,
    const std::vector<Object::Ref<MeshBufferBase> >& buffers) {
  auto index_size = index_sizes.begin();
  auto buffer = buffers.begin();
  for (auto&& mesh : meshes) {
    // For each mesh, plug in the latest and greatest buffers it should be
    // using.
    MeshData* mesh_data = mesh->mesh_data;
    switch (mesh_data->type()) {
      case MeshDataType::kIndexedSimpleSplit: {
        GET_MESH_DATA(MeshDataSimpleSplitGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexSimpleSplitStatic>, static_data);
        GET_BUFFER(MeshBuffer<VertexSimpleSplitDynamic>, dynamic_data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetStaticData(static_data);
        m->SetDynamicData(dynamic_data);
        break;
      }
      case MeshDataType::kIndexedObjectSplit: {
        GET_MESH_DATA(MeshDataObjectSplitGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexObjectSplitStatic>, static_data);
        GET_BUFFER(MeshBuffer<VertexObjectSplitDynamic>, dynamic_data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetStaticData(static_data);
        m->SetDynamicData(dynamic_data);
        break;
      }
      case MeshDataType::kIndexedSimpleFull: {
        GET_MESH_DATA(MeshDataSimpleFullGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexSimpleFull>, data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetData(data);
        break;
      }
      case MeshDataType::kIndexedDualTextureFull: {
        GET_MESH_DATA(MeshDataDualTextureFullGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexDualTextureFull>, data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetData(data);
        break;
      }
      case MeshDataType::kIndexedSmokeFull: {
        GET_MESH_DATA(MeshDataSmokeFullGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexSmokeFull>, data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetData(data);
        break;
      }
      case MeshDataType::kSprite: {
        GET_MESH_DATA(MeshDataSpriteGL, m);
        GET_INDEX_BUFFER();
        GET_BUFFER(MeshBuffer<VertexSprite>, data);
        if (use_indices32) {
          m->SetIndexData(indices32);
        } else {
          m->SetIndexData(indices16);
        }
        m->SetData(data);
        break;
      }
      default:
        throw Exception("Invalid meshdata type: "
                        + std::to_string(static_cast<int>(mesh_data->type())));
    }
  }
  // We should have gone through all lists exactly.
  assert(index_size == index_sizes.end());
  assert(buffer == buffers.end());
}
#undef GET_MESH_DATA
#undef GET_BUFFER
#undef GET_INDEX_BUFFER

void RendererGL::StandardPostProcessSetup_(ProgramPostProcessGL* p,
                                           const RenderPass& pass) {
  auto* cam_target = static_cast<RenderTargetGL*>(camera_render_target());
  assert(cam_target
         && dynamic_cast<RenderTargetGL*>(camera_render_target())
                == cam_target);
  RenderPass* beauty_pass = pass.frame_def()->beauty_pass();
  assert(beauty_pass);
  SetDoubleSided_(false);
  SetBlend(false);
  p->Bind();
  p->SetColorTexture(cam_target->framebuffer()->texture());
  if (p->UsesSlightBlurredTex()) {
    p->SetColorSlightBlurredTexture(blur_buffers_[0]->texture());
  }
  if (blur_buffers_.size() > 1) {
    if (p->UsesBlurredTexture()) {
      p->SetColorBlurredTexture(blur_buffers_[1]->texture());
    }
    p->SetColorBlurredMoreTexture(
        blur_buffers_[blur_buffers_.size() - 1]->texture());
  } else {
    if (p->UsesBlurredTexture()) {
      p->SetColorBlurredTexture(blur_buffers_[0]->texture());
    }
    p->SetColorBlurredMoreTexture(blur_buffers_[0]->texture());
  }
  p->SetDepthTexture(cam_target->framebuffer()->depth_texture());
  float dof_near_smoothed = this->dof_near_smoothed();
  float dof_far_smoothed = this->dof_far_smoothed();

  // FIXME: These sort of fudge-factors don't belong here in the renderer.
  if (pass.frame_def()->orbiting()) {
    p->SetDepthOfFieldRanges(
        GetZBufferValue(beauty_pass, 0.80f * dof_near_smoothed),
        GetZBufferValue(beauty_pass, 0.91f * dof_near_smoothed),
        GetZBufferValue(beauty_pass, 1.01f * dof_far_smoothed),
        GetZBufferValue(beauty_pass, 1.10f * dof_far_smoothed));
  } else {
    p->SetDepthOfFieldRanges(
        GetZBufferValue(beauty_pass, 0.93f * dof_near_smoothed),
        GetZBufferValue(beauty_pass, 0.99f * dof_near_smoothed),
        GetZBufferValue(beauty_pass, 1.03f * dof_far_smoothed),
        GetZBufferValue(beauty_pass, 1.09f * dof_far_smoothed));
  }
}

void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer,
                                            const RenderPass& pass,
                                            RenderTarget* render_target) {
  buffer->ReadBegin();
  RenderCommandBuffer::Command cmd;
  while ((cmd = buffer->GetCommand()) != RenderCommandBuffer::Command::kEnd) {
    switch (cmd) {
      case RenderCommandBuffer::Command::kEnd:
        break;
      case RenderCommandBuffer::Command::kShader: {
        auto shader = static_cast<ShadingType>(buffer->GetInt());
        switch (shader) {
          case ShadingType::kSimpleColor: {
            SetDoubleSided_(false);
            SetBlend(false);
            ProgramSimpleGL* p = simple_color_prog_;
            p->Bind();
            float r, g, b;
            buffer->GetFloats(&r, &g, &b);
            p->SetColor(r, g, b);
            break;
          }
          case ShadingType::kSimpleColorTransparent: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            ProgramSimpleGL* p = simple_color_prog_;
            p->Bind();
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            p->SetColor(r, g, b, a);
            break;
          }
          case ShadingType::kSimpleColorTransparentDoubleSided: {
            SetDoubleSided_(true);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            ProgramSimpleGL* p = simple_color_prog_;
            p->Bind();
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            p->SetColor(r, g, b, a);
            break;
          }
          case ShadingType::kSimpleTexture: {
            SetDoubleSided_(false);
            SetBlend(false);
            ProgramSimpleGL* p = simple_tex_prog_;
            p->Bind();
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparent: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramSimpleGL* p = simple_tex_mod_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransFlatness: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, flatness;
            buffer->GetFloats(&r, &g, &b, &a, &flatness);
            ProgramSimpleGL* p = simple_tex_mod_flatness_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetFlatness(flatness);
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentShadow: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, shadow_offset_x, shadow_offset_y, shadow_blur,
                shadow_opacity;
            buffer->GetFloats(&r, &g, &b, &a, &shadow_offset_x,
                              &shadow_offset_y, &shadow_blur, &shadow_opacity);
            ProgramSimpleGL* p = simple_tex_mod_shadow_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            const TextureAsset* t = buffer->GetTexture();
            const TextureAsset* t_mask = buffer->GetTexture();
            p->SetColorTexture(t);
            // If this isn't a full-res texture, ramp down the blurring we do.
            p->SetShadow(shadow_offset_x, shadow_offset_y,
                         std::max(0.0f, shadow_blur), shadow_opacity);
            p->SetMaskUV2Texture(t_mask);
            break;
          }
          case ShadingType::kSimpleTexModulatedTransShadowFlatness: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, shadow_offset_x, shadow_offset_y, shadow_blur,
                shadow_opacity, flatness;
            buffer->GetFloats(&r, &g, &b, &a, &shadow_offset_x,
                              &shadow_offset_y, &shadow_blur, &shadow_opacity,
                              &flatness);
            ProgramSimpleGL* p = simple_tex_mod_shadow_flatness_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            const TextureAsset* t = buffer->GetTexture();
            const TextureAsset* t_mask = buffer->GetTexture();
            p->SetColorTexture(t);
            // If this isn't a full-res texture, ramp down the blurring we do.
            p->SetShadow(shadow_offset_x, shadow_offset_y,
                         std::max(0.0f, shadow_blur), shadow_opacity);
            p->SetMaskUV2Texture(t_mask);
            p->SetFlatness(flatness);
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentGlow: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, glow_amount, glow_blur;
            buffer->GetFloats(&r, &g, &b, &a, &glow_amount, &glow_blur);
            ProgramSimpleGL* p = simple_tex_mod_glow_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            const TextureAsset* t = buffer->GetTexture();
            p->SetColorTexture(t);

            // Glow.
            p->SetGlow(glow_amount, std::max(0.0f, glow_blur));
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, glow_amount, glow_blur;
            buffer->GetFloats(&r, &g, &b, &a, &glow_amount, &glow_blur);
            ProgramSimpleGL* p = simple_tex_mod_glow_maskuv2_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            const TextureAsset* t = buffer->GetTexture();
            p->SetColorTexture(t);
            const TextureAsset* t_mask = buffer->GetTexture();
            p->SetMaskUV2Texture(t_mask);
            // Glow.
            p->SetGlow(glow_amount, std::max(0.0f, glow_blur));
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentDoubleSided: {
            SetDoubleSided_(true);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramSimpleGL* p = simple_tex_mod_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulated: {
            SetDoubleSided_(false);
            SetBlend(false);
            float r, g, b;
            buffer->GetFloats(&r, &g, &b);
            ProgramSimpleGL* p = simple_tex_mod_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedColorized: {
            SetDoubleSided_(false);
            SetBlend(false);
            float r, g, b, colorize_r, colorize_g, colorize_b;
            buffer->GetFloats(&r, &g, &b, &colorize_r, &colorize_g,
                              &colorize_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorizeTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedColorized2: {
            SetDoubleSided_(false);
            SetBlend(false);
            float r, g, b, colorize_r, colorize_g, colorize_b, colorize2_r,
                colorize2_g, colorize2_b;
            buffer->GetFloats(&r, &g, &b, &colorize_r, &colorize_g, &colorize_b,
                              &colorize2_r, &colorize2_g, &colorize2_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized2_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);
            break;
          }
          case ShadingType::kSimpleTextureModulatedColorized2Masked: {
            SetDoubleSided_(false);
            SetBlend(false);
            float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r,
                colorize2_g, colorize2_b;
            buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g,
                              &colorize_b, &colorize2_r, &colorize2_g,
                              &colorize2_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized2_masked_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);
            p->SetColorizeTexture(buffer->GetTexture());
            p->SetMaskTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentColorized: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, colorize_r, colorize_g, colorize_b;
            buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g,
                              &colorize_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorizeTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSimpleTextureModulatedTransparentColorized2: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r,
                colorize2_g, colorize2_b;
            buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g,
                              &colorize_b, &colorize2_r, &colorize2_g,
                              &colorize2_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized2_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);
            p->SetColorizeTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::
              kSimpleTextureModulatedTransparentColorized2Masked: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r,
                colorize2_g, colorize2_b;
            buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g,
                              &colorize_b, &colorize2_r, &colorize2_g,
                              &colorize2_b);
            ProgramSimpleGL* p = simple_tex_mod_colorized2_masked_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);
            p->SetColorizeTexture(buffer->GetTexture());
            p->SetMaskTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kObject: {
            SetDoubleSided_(false);
            SetBlend(false);
            float r, g, b;
            buffer->GetFloats(&r, &g, &b);
            ProgramObjectGL* p = obj_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetVignetteTexture(vignette_tex_);
            break;
          }
          case ShadingType::kSmoke: {
            SetDoubleSided_(true);
            SetBlend(true);
            SetBlendPremult(true);
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramSmokeGL* p = smoke_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kSmokeOverlay: {
            SetDoubleSided_(true);
            SetBlend(true);
            SetBlendPremult(true);
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramSmokeGL* p = smoke_overlay_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetDepthTexture(
                static_cast<RenderTargetGL*>(camera_render_target())
                    ->framebuffer()
                    ->depth_texture());
            p->SetBlurTexture(
                blur_buffers_[blur_buffers_.size() - 1]->texture());
            break;
          }
          case ShadingType::kPostProcessNormalDistort: {
            float distort = buffer->GetFloat();
            ProgramPostProcessGL* p = postprocess_distort_prog_;
            StandardPostProcessSetup_(p, pass);
            p->SetDistort(distort);
            break;
          }
          case ShadingType::kPostProcess: {
            ProgramPostProcessGL* p = postprocess_prog_;
            StandardPostProcessSetup_(p, pass);
            break;
          }
          case ShadingType::kPostProcessEyes: {
            assert(postprocess_eyes_prog_);
            ProgramPostProcessGL* p = postprocess_eyes_prog_;
            StandardPostProcessSetup_(p, pass);
            break;
          }
          case ShadingType::kSprite: {
            SetDoubleSided_(false);
            SetBlend(true);
            SetBlendPremult(true);

            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            bool overlay = static_cast<bool>(buffer->GetInt());
            bool cam_aligned = static_cast<bool>(buffer->GetInt());

            ProgramSpriteGL* p;
            if (cam_aligned) {
              if (overlay) {
                p = sprite_camalign_overlay_prog_;
              } else {
                p = sprite_camalign_prog_;
              }
            } else {
              assert(!overlay);  // Unsupported combo.
              p = sprite_prog_;
            }
            p->Bind();
            if (overlay) {
              p->SetDepthTexture(
                  static_cast<RenderTargetGL*>(camera_render_target())
                      ->framebuffer()
                      ->depth_texture());
            }
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            break;
          }
          case ShadingType::kObjectTransparent: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());

            SetBlend(true);
            SetBlendPremult(premult);

            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramObjectGL* p = obj_transparent_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetVignetteTexture(vignette_tex_);
            break;
          }
          case ShadingType::kObjectLightShadow: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            int world_space = buffer->GetInt();
            float r, g, b;
            buffer->GetFloats(&r, &g, &b);
            ProgramObjectGL* p = world_space ? obj_lightshad_worldspace_prog_
                                             : obj_lightshad_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectLightShadowTransparent: {
            SetDoubleSided_(false);
            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            float r, g, b, a;
            buffer->GetFloats(&r, &g, &b, &a);
            ProgramObjectGL* p = obj_lightshad_transparent_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);

            break;
          }
          case ShadingType::kObjectReflectLightShadow: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            int world_space = buffer->GetInt();
            float r, g, b, reflect_r, reflect_g, reflect_b;
            buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b);
            ProgramObjectGL* p = world_space
                                     ? obj_refl_lightshad_worldspace_prog_
                                     : obj_refl_lightshad_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowDoubleSided: {
            // FIXME: This shader isn't actually flipping the normal for the
            //  back side of the face.. for now we don't care though.
            SetDoubleSided_(true);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            int world_space = buffer->GetInt();

            // Verified.
            float r, g, b, reflect_r, reflect_g, reflect_b;
            buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b);
            ProgramObjectGL* p;

            // Testing why reflection is wonky..
            if (explicit_bool(false)) {
              p = world_space ? obj_lightshad_worldspace_prog_
                              : obj_lightshad_prog_;
              p->Bind();
              p->SetColor(r, g, b);
              p->SetColorTexture(buffer->GetTexture());
              buffer->GetTexture();
            } else {
              p = world_space ? obj_refl_lightshad_worldspace_prog_
                              : obj_refl_lightshad_prog_;
              p->Bind();
              p->SetColor(r, g, b);
              p->SetColorTexture(buffer->GetTexture());
              p->SetReflectionTexture(buffer->GetTexture());
              p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            }
            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowColorized: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            float r, g, b, reflect_r, reflect_g, reflect_b, colorize_r,
                colorize_g, colorize_b;
            buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b,
                              &colorize_r, &colorize_g, &colorize_b);
            ProgramObjectGL* p = obj_refl_lightshad_colorize_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetColorizeTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowColorized2: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());

            float r, g, b, reflect_r, reflect_g, reflect_b, colorize_r,
                colorize_g, colorize_b, colorize2_r, colorize2_g, colorize2_b;
            buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b,
                              &colorize_r, &colorize_g, &colorize_b,
                              &colorize2_r, &colorize2_g, &colorize2_b);
            ProgramObjectGL* p = obj_refl_lightshad_colorize2_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());

            p->SetColorizeTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);

            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);

            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowAdd: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());
            float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b;
            buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r,
                              &reflect_g, &reflect_b);
            ProgramObjectGL* p = obj_refl_lightshad_add_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetAddColor(add_r, add_g, add_b);
            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);

            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowAddColorized: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());

            float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b,
                colorize_r, colorize_g, colorize_b;
            buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r,
                              &reflect_g, &reflect_b, &colorize_r, &colorize_g,
                              &colorize_b);
            ProgramObjectGL* p = obj_refl_lightshad_add_colorize_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetAddColor(add_r, add_g, add_b);

            p->SetColorizeTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);

            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);

            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflectLightShadowAddColorized2: {
            SetDoubleSided_(false);
            SetBlend(false);
            auto light_shadow = static_cast<LightShadowType>(buffer->GetInt());

            float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b,
                colorize_r, colorize_g, colorize_b, colorize2_r, colorize2_g,
                colorize2_b;
            buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r,
                              &reflect_g, &reflect_b, &colorize_r, &colorize_g,
                              &colorize_b, &colorize2_r, &colorize2_g,
                              &colorize2_b);
            ProgramObjectGL* p = obj_refl_lightshad_add_colorize2_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetAddColor(add_r, add_g, add_b);

            p->SetColorizeTexture(buffer->GetTexture());
            p->SetColorizeColor(colorize_r, colorize_g, colorize_b);
            p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b);

            p->SetReflectionTexture(buffer->GetTexture());
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);

            p->SetVignetteTexture(vignette_tex_);
            GLuint light_shadow_tex;
            switch (light_shadow) {
              case LightShadowType::kTerrain:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              case LightShadowType::kObject:
                light_shadow_tex =
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture();
                break;
              default:
                light_shadow_tex = 0;
                FatalError("Unhandled LightShadowType.");
            }
            p->SetLightShadowTexture(light_shadow_tex);
            break;
          }
          case ShadingType::kObjectReflect: {
            SetDoubleSided_(false);
            SetBlend(false);
            int world_space = buffer->GetInt();
            // verified
            float r, g, b, reflect_r, reflect_g, reflect_b;
            buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b);
            ProgramObjectGL* p =
                world_space ? obj_refl_worldspace_prog_ : obj_refl_prog_;
            p->Bind();
            p->SetColor(r, g, b);
            p->SetColorTexture(buffer->GetTexture());
            p->SetReflectionTexture(buffer->GetTexture());  // reflection
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            break;
          }
          case ShadingType::kObjectReflectTransparent: {
            SetDoubleSided_(false);

            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, reflect_r, reflect_g, reflect_b;
            buffer->GetFloats(&r, &g, &b, &a, &reflect_r, &reflect_g,
                              &reflect_b);
            ProgramObjectGL* p = obj_refl_transparent_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetReflectionTexture(buffer->GetTexture());  // reflection
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            break;
          }
          case ShadingType::kObjectReflectAddTransparent: {
            SetDoubleSided_(false);

            bool premult = static_cast<bool>(buffer->GetInt());
            SetBlend(true);
            SetBlendPremult(premult);
            float r, g, b, a, add_r, add_g, add_b, reflect_r, reflect_g,
                reflect_b;
            buffer->GetFloats(&r, &g, &b, &a, &add_r, &add_g, &add_b,
                              &reflect_r, &reflect_g, &reflect_b);
            ProgramObjectGL* p = obj_refl_add_transparent_prog_;
            p->Bind();
            p->SetColor(r, g, b, a);
            p->SetColorTexture(buffer->GetTexture());
            p->SetAddColor(add_r, add_g, add_b);
            p->SetReflectionTexture(buffer->GetTexture());  // reflection
            p->SetReflectionMult(reflect_r, reflect_g, reflect_b);
            break;
          }
          case ShadingType::kShield: {
            SetDoubleSided_(true);
            SetBlend(true);
            SetBlendPremult(true);
            ProgramShieldGL* p = shield_prog_;
            p->Bind();
            p->SetDepthTexture(
                static_cast<RenderTargetGL*>(camera_render_target())
                    ->framebuffer()
                    ->depth_texture());
            break;
          }
          case ShadingType::kSpecial: {
            SetDoubleSided_(false);

            // if we ever need to use non-blend version
            // of this in real renders, we should split off a non-blend version
            SetBlend(true);
            SetBlendPremult(true);
            auto source = (SpecialComponent::Source)buffer->GetInt();
            ProgramSimpleGL* p = simple_tex_mod_prog_;
            p->Bind();
            switch (source) {
              case SpecialComponent::Source::kLightBuffer:
                p->SetColorTexture(
                    static_cast<RenderTargetGL*>(light_render_target())
                        ->framebuffer()
                        ->texture());
                break;
              case SpecialComponent::Source::kLightShadowBuffer:
                p->SetColorTexture(
                    static_cast<RenderTargetGL*>(light_shadow_render_target())
                        ->framebuffer()
                        ->texture());
                break;
              case SpecialComponent::Source::kVROverlayBuffer: {
                p->SetColorTexture(static_cast<RenderTargetGL*>(
                                       vr_overlay_flat_render_target())
                                       ->framebuffer()
                                       ->texture());
                p->SetColor(1, 1, 1, 0.95f);
                break;
              }
              default:
                FatalError("Unhandled SpecialComponent type.");
            }
            break;
          }
          default:
            FatalError("Unhandled Shader Type.");
        }
        break;
      }
      case RenderCommandBuffer::Command::kSimpleComponentInlineColor: {
        float r, g, b, a;
        buffer->GetFloats(&r, &g, &b, &a);
        auto* p = static_cast<ProgramSimpleGL*>(GetActiveProgram_());
        assert(p != nullptr
               && p == dynamic_cast<ProgramSimpleGL*>(GetActiveProgram_()));
        p->SetColor(r, g, b, a);
        break;
      }
      case RenderCommandBuffer::Command::kObjectComponentInlineColor: {
        float r, g, b, a;
        buffer->GetFloats(&r, &g, &b, &a);
        auto* p = static_cast<ProgramObjectGL*>(GetActiveProgram_());
        assert(p != nullptr
               && p == dynamic_cast<ProgramObjectGL*>(GetActiveProgram_()));
        p->SetColor(r, g, b, a);
        break;
      }
      case RenderCommandBuffer::Command::kObjectComponentInlineAddColor: {
        float r, g, b;
        buffer->GetFloats(&r, &g, &b);
        auto* p = static_cast<ProgramObjectGL*>(GetActiveProgram_());
        assert(p != nullptr
               && p == dynamic_cast<ProgramObjectGL*>(GetActiveProgram_()));
        p->SetAddColor(r, g, b);
        break;
      }
      case RenderCommandBuffer::Command::kDrawMeshAsset: {
        int flags = buffer->GetInt();
        const MeshAsset* m = buffer->GetMesh();
        assert(m);
        auto mesh =
            static_cast_check_type<MeshAssetDataGL*>(m->renderer_data());
        assert(mesh);

        // if they don't wanna draw in reflections...
        if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) {
          break;
        }
        GetActiveProgram_()->PrepareToDraw();
        mesh->Bind();
        mesh->Draw();
        break;
      }
      case RenderCommandBuffer::Command::kDrawMeshAssetInstanced: {
        int flags = buffer->GetInt();
        const MeshAsset* m = buffer->GetMesh();
        assert(m);
        auto mesh =
            static_cast_check_type<MeshAssetDataGL*>(m->renderer_data());
        assert(mesh);
        Matrix44f* mats;
        int count;
        mats = buffer->GetMatrices(&count);
        // if they don't wanna draw in reflections...
        if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) {
          break;
        }
        mesh->Bind();
        for (int i = 0; i < count; i++) {
          g_base->graphics_server->PushTransform();
          g_base->graphics_server->MultMatrix(mats[i]);
          GetActiveProgram_()->PrepareToDraw();
          mesh->Draw();
          g_base->graphics_server->PopTransform();
        }
        break;
      }
        // NOLINTNEXTLINE(bugprone-branch-clone)
      case RenderCommandBuffer::Command::kBeginDebugDrawTriangles: {
        GetActiveProgram_()->PrepareToDraw();
#if BA_GL_ENABLE_DEBUG_DRAW_COMMANDS
        glBegin(GL_TRIANGLES);
#endif
        break;
      }
      case RenderCommandBuffer::Command::kBeginDebugDrawLines: {
        GetActiveProgram_()->PrepareToDraw();
#if BA_GL_ENABLE_DEBUG_DRAW_COMMANDS
        glBegin(GL_LINES);
#endif
        break;
      }
      case RenderCommandBuffer::Command::kEndDebugDraw: {
#if BA_GL_ENABLE_DEBUG_DRAW_COMMANDS
        glEnd();
#endif
        break;
      }
      case RenderCommandBuffer::Command::kDebugDrawVertex3: {
        float x, y, z;
        buffer->GetFloats(&x, &y, &z);
#if BA_GL_ENABLE_DEBUG_DRAW_COMMANDS
        glVertex3f(x, y, z);
#endif
        break;
      }
      case RenderCommandBuffer::Command::kDrawMesh: {
        int flags = buffer->GetInt();
        auto* mesh = buffer->GetMeshRendererData<MeshDataGL>();
        assert(mesh);
        if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) {
          break;
        }
        GetActiveProgram_()->PrepareToDraw();
        mesh->Bind();
        mesh->Draw(DrawType::kTriangles);
        break;
      }
      case RenderCommandBuffer::Command::kDrawScreenQuad: {
        // Save proj/mv matrices, set up to draw a simple screen quad at the
        // back of our depth range, draw, and restore.
        Matrix44f old_model_view_matrix =
            g_base->graphics_server->model_view_matrix();
        Matrix44f old_projection_matrix =
            g_base->graphics_server->projection_matrix();
        g_base->graphics_server->SetModelViewMatrix(kMatrix44fIdentity);
        g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 0.01f);
        GetActiveProgram_()->PrepareToDraw();
        screen_mesh_->Bind();
        screen_mesh_->Draw(DrawType::kTriangles);
        g_base->graphics_server->SetModelViewMatrix(old_model_view_matrix);
        g_base->graphics_server->SetProjectionMatrix(old_projection_matrix);
        break;
      }
      case RenderCommandBuffer::Command::kScissorPush: {
        Rect r;
        buffer->GetFloats(&r.l, &r.b, &r.r, &r.t);

        // Convert scissor-values from model space to view space.
        // this of course assumes there's no rotations and whatnot.
        Vector3f bot_left_pt = g_base->graphics_server->model_view_matrix()
                               * Vector3f(r.l, r.b, 0);
        Vector3f top_right_pt = g_base->graphics_server->model_view_matrix()
                                * Vector3f(r.r, r.t, 0);
        r.l = bot_left_pt.x;
        r.b = bot_left_pt.y;
        r.r = top_right_pt.x;
        r.t = top_right_pt.y;
        ScissorPush_(r, render_target);
        break;
      }
      case RenderCommandBuffer::Command::kScissorPop: {
        ScissorPop_(render_target);
        break;
      }
      case RenderCommandBuffer::Command::kPushTransform: {
        g_base->graphics_server->PushTransform();
        break;
      }
      case RenderCommandBuffer::Command::kTranslate2: {
        float x, y;
        buffer->GetFloats(&x, &y);
        g_base->graphics_server->Translate(Vector3f(x, y, 0));
        break;
      }
      case RenderCommandBuffer::Command::kTranslate3: {
        float x, y, z;
        buffer->GetFloats(&x, &y, &z);
        g_base->graphics_server->Translate(Vector3f(x, y, z));
        break;
      }
      case RenderCommandBuffer::Command::kCursorTranslate: {
        float x, y;
        g_base->app_adapter->CursorPositionForDraw(&x, &y);
        g_base->graphics_server->Translate(Vector3f(x, y, 0));
        break;
      }
      case RenderCommandBuffer::Command::kScale2: {
        float x, y;
        buffer->GetFloats(&x, &y);
        g_base->graphics_server->scale(Vector3f(x, y, 1.0f));
        break;
      }
      case RenderCommandBuffer::Command::kScale3: {
        float x, y, z;
        buffer->GetFloats(&x, &y, &z);
        g_base->graphics_server->scale(Vector3f(x, y, z));
        break;
      }
      case RenderCommandBuffer::Command::kScaleUniform: {
        float s = buffer->GetFloat();
        g_base->graphics_server->scale(Vector3f(s, s, s));
        break;
      }
#if BA_VR_BUILD
      case RenderCommandBuffer::Command::kTransformToRightHand: {
        VRTransformToRightHand();
        break;
      }
      case RenderCommandBuffer::Command::kTransformToLeftHand: {
        VRTransformToLeftHand();
        break;
      }
      case RenderCommandBuffer::Command::kTransformToHead: {
        VRTransformToHead();
        break;
      }
#endif  // BA_VR_BUILD
      case RenderCommandBuffer::Command::kTranslateToProjectedPoint: {
        float x, y, z;
        buffer->GetFloats(&x, &y, &z);
        Vector3f t = pass.frame_def()->beauty_pass()->tex_project_matrix()
                     * Vector3f(x, y, z);
        g_base->graphics_server->Translate(Vector3f(
            t.x * g_base->graphics_server->screen_virtual_width(),
            t.y * g_base->graphics_server->screen_virtual_height(), 0));
        break;
      }
      case RenderCommandBuffer::Command::kRotate: {
        float angle, x, y, z;
        buffer->GetFloats(&angle, &x, &y, &z);
        g_base->graphics_server->Rotate(angle, Vector3f(x, y, z));
        break;
      }
      case RenderCommandBuffer::Command::kMultMatrix: {
        g_base->graphics_server->MultMatrix(*(buffer->GetMatrix()));
        break;
      }
      case RenderCommandBuffer::Command::kPopTransform: {
        g_base->graphics_server->PopTransform();
        break;
      }
      case RenderCommandBuffer::Command::kFlipCullFace: {
        FlipCullFace();
        break;
      }
      default:
        throw Exception("Invalid command in render-command-buffer");
    }
  }
  assert(buffer->IsEmpty());
}  // NOLINT (yes this is too long)

void RendererGL::BlitBuffer(RenderTarget* src_in, RenderTarget* dst_in,
                            bool depth, bool linear_interpolation,
                            bool force_shader_mode, bool invalidate_source) {
  BA_DEBUG_CHECK_GL_ERROR;
  auto* src = static_cast<RenderTargetGL*>(src_in);
  assert(src && src == dynamic_cast<RenderTargetGL*>(src_in));
  auto* dst = static_cast<RenderTargetGL*>(dst_in);
  assert(dst && dst == dynamic_cast<RenderTargetGL*>(dst_in));

  bool do_shader_blit{true};

  // If they want depth we *MUST* use glBlitFramebuffer and can't have linear
  // interp.
  if (depth) {
    assert(!force_shader_mode);
    linear_interpolation = false;
  }
  // Use glBlitFramebuffer when its available.
  // FIXME: This should be available in ES3.
  // #if !BA_OSTYPE_IOS_TVOS
  if (!force_shader_mode) {
    do_shader_blit = false;
    BA_DEBUG_CHECK_GL_ERROR;
    glBindFramebuffer(GL_READ_FRAMEBUFFER, src->GetFramebufferID());
    BA_DEBUG_CHECK_GL_ERROR;
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst->GetFramebufferID());
    BA_DEBUG_CHECK_GL_ERROR;

    glBlitFramebuffer(0, 0, static_cast<GLint>(src->physical_width()),
                      static_cast<GLint>(src->physical_height()), 0, 0,
                      static_cast<GLint>(dst->physical_width()),
                      static_cast<GLint>(dst->physical_height()),
                      depth ? (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
                            : GL_COLOR_BUFFER_BIT,
                      linear_interpolation ? GL_LINEAR : GL_NEAREST);
    BA_DEBUG_CHECK_GL_ERROR;
    if (invalidate_source) {
      InvalidateFramebuffer(true, depth, true);
    }
  } else {
    do_shader_blit = true;
  }
  // #endif
  if (do_shader_blit) {
    SetDepthWriting(false);
    SetDepthTesting(false);
    dst_in->DrawBegin(false);
    g_base->graphics_server->ModelViewReset();
    g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1);

    // Copied from ShadingType::kSimpleColor
    SetDoubleSided_(false);
    SetBlend(false);
    ProgramSimpleGL* p = simple_tex_prog_;
    p->Bind();
    p->SetColorTexture(src->framebuffer()->texture());
    GetActiveProgram_()->PrepareToDraw();
    screen_mesh_->Bind();
    screen_mesh_->Draw(DrawType::kTriangles);
    BA_DEBUG_CHECK_GL_ERROR;
  }
}

void RendererGL::ScissorPush_(const Rect& r_in, RenderTarget* render_target) {
  if (scissor_rects_.empty()) {
    glEnable(GL_SCISSOR_TEST);
    scissor_rects_.push_back(r_in);
  } else {
    Rect r;
    Rect rp = scissor_rects_.back();
    r.l = r_in.l > rp.l ? r_in.l : rp.l;
    r.r = r_in.r < rp.r ? r_in.r : rp.r;
    r.b = r_in.b > rp.b ? r_in.b : rp.b;
    r.t = r_in.t < rp.t ? r_in.t : rp.t;
    scissor_rects_.push_back(r);
  }
  Rect clip = scissor_rects_.back();
  if (clip.l > clip.r) {
    clip.l = clip.r;
  }
  if (clip.b > clip.t) {
    clip.b = clip.t;
  }
  auto* glt = static_cast<RenderTargetGL*>(render_target);
  float scissor_scale_x =
      static_cast<RenderTargetGL*>(render_target)->GetScissorScaleX();
  float scissor_scale_y =
      static_cast<RenderTargetGL*>(render_target)->GetScissorScaleY();
  glScissor(static_cast<GLint>(glt->GetScissorX(clip.l)),
            static_cast<GLint>(glt->GetScissorY(clip.b)),
            static_cast<GLsizei>(scissor_scale_x * (clip.r - clip.l)),
            static_cast<GLsizei>(scissor_scale_y * (clip.t - clip.b)));
  BA_DEBUG_CHECK_GL_ERROR;
}

void RendererGL::ScissorPop_(RenderTarget* render_target) {
  BA_PRECONDITION(!scissor_rects_.empty());
  scissor_rects_.pop_back();
  if (scissor_rects_.empty()) {
    glDisable(GL_SCISSOR_TEST);
  } else {
    Rect clip = scissor_rects_.back();
    if (clip.l > clip.r) {
      clip.l = clip.r;
    }
    if (clip.b > clip.t) {
      clip.b = clip.t;
    }
    auto* glt = static_cast<RenderTargetGL*>(render_target);
    float scissor_scale_x =
        static_cast<RenderTargetGL*>(render_target)->GetScissorScaleX();
    float scissor_scale_y =
        static_cast<RenderTargetGL*>(render_target)->GetScissorScaleY();
    glScissor(static_cast<GLint>(glt->GetScissorX(clip.l)),
              static_cast<GLint>(glt->GetScissorY(clip.b)),
              static_cast<GLsizei>(scissor_scale_x * (clip.r - clip.l)),
              static_cast<GLsizei>(scissor_scale_y * (clip.t - clip.b)));
  }
  BA_DEBUG_CHECK_GL_ERROR;
}

void RendererGL::SetDepthWriting(bool enable) {
  if (enable != depth_writing_enabled_) {
    depth_writing_enabled_ = enable;
    glDepthMask(static_cast<GLboolean>(enable));
  }
}

void RendererGL::SetDrawAtEqualDepth(bool enable) {
  if (enable != draw_at_equal_depth_) {
    draw_at_equal_depth_ = enable;
    if (enable) {
      glDepthFunc(GL_LEQUAL);
    } else {
      glDepthFunc(GL_LESS);
    }
  }
}

// FIXME FIXME FIXME FIXME:
//
// Turning off GL_DEPTH_TEST also disables depth writing which we may not
// want. It sounds like the proper thing to do in that case is leave
// GL_DEPTH_TEST on and set glDepthFunc(GL_ALWAYS).

void RendererGL::SetDepthTesting(bool enable) {
  if (enable != depth_testing_enabled_) {
    depth_testing_enabled_ = enable;
    if (enable) {
      glEnable(GL_DEPTH_TEST);
    } else {
      glDisable(GL_DEPTH_TEST);
    }
  }
}

void RendererGL::SetDepthRange(float min, float max) {
  if (min != depth_range_min_ || max != depth_range_max_) {
    depth_range_min_ = min;
    depth_range_max_ = max;
    glDepthRange(min, max);
  }
}

void RendererGL::FlipCullFace() {
  draw_front_ = !draw_front_;
  if (draw_front_) {
    glCullFace(GL_BACK);
  } else {
    glCullFace(GL_FRONT);
  }
}

void RendererGL::SetBlend(bool b) {
#if !BA_GL_ENABLE_BLEND
  b = false;
#endif
  if (blend_ != b) {
    blend_ = b;
    if (blend_) {
      glEnable(GL_BLEND);
    } else {
      glDisable(GL_BLEND);
    }
  }
}

void RendererGL::SetBlendPremult(bool b) {
  if (blend_premult_ != b) {
    blend_premult_ = b;
    if (blend_premult_) {
      glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    } else {
      // currently we only ever write to an alpha buffer for our vr overlay
      // texture, and in that case we need alpha to accumulate; not get
      // overwritten. could probably enable this everywhere but I don't know if
      // it's supported on all hardware or is slower or whatnot..
      if (g_core->vr_mode()) {
        glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                            GL_ONE_MINUS_SRC_ALPHA);
      } else {
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
      }
    }
  }
}

void RendererGL::BindVertexArray_(GLuint v) {
  if (v != current_vertex_array_) {
    glBindVertexArray(v);
    BA_DEBUG_CHECK_GL_ERROR;
    current_vertex_array_ = v;
  }
}

void RendererGL::SetDoubleSided_(bool d) {
  if (double_sided_ != d) {
    double_sided_ = d;
    if (double_sided_) {
      glDisable(GL_CULL_FACE);
    } else {
      glEnable(GL_CULL_FACE);
    }
  }
}

void RendererGL::UpdateVignetteTex_(bool force) {
  if (force || vignette_quality_ != g_base->graphics_server->quality()
      || vignette_tex_outer_r_ != vignette_outer().x
      || vignette_tex_outer_g_ != vignette_outer().y
      || vignette_tex_outer_b_ != vignette_outer().z
      || vignette_tex_inner_r_ != vignette_inner().x
      || vignette_tex_inner_g_ != vignette_inner().y
      || vignette_tex_inner_b_ != vignette_inner().z) {
    vignette_tex_outer_r_ = vignette_outer().x;
    vignette_tex_outer_g_ = vignette_outer().y;
    vignette_tex_outer_b_ = vignette_outer().z;
    vignette_tex_inner_r_ = vignette_inner().x;
    vignette_tex_inner_g_ = vignette_inner().y;
    vignette_tex_inner_b_ = vignette_inner().z;
    vignette_quality_ = g_base->graphics_server->quality();

    const int width = 64;
    const int height = 64;
    const size_t tex_buffer_size = width * height * 4;
    std::vector<uint8_t> datavec(tex_buffer_size);
    uint8_t* data{datavec.data()};
    float max_c = 0.5f * 0.5f * 0.5f * 0.5f;
    uint8_t* b = data;

    float out_r = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_r_)));
    float out_g = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_g_)));
    float out_b = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_b_)));
    float in_r = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_r_)));
    float in_g = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_g_)));
    float in_b = std::min(
        255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_b_)));

    for (int y = 0; y < height; y++) {
      float d3 = static_cast<float>(y) / (height - 1);
      float d4 = 1.0f - d3;
      for (int x = 0; x < width; x++) {
        float d1 = static_cast<float>(x) / (width - 1);
        float d2 = 1.0f - d1;
        float c = 1.0f * (1.0f - ((d1 * d2 * d3 * d4) / max_c));
        c = 0.5f * (c * c) + 0.5f * c;
        c = std::min(1.0f, std::max(0.0f, c));

        b[0] = static_cast<uint8_t>(c * out_r + (1.0f - c) * in_r);
        b[1] = static_cast<uint8_t>(c * out_g + (1.0f - c) * in_g);
        b[2] = static_cast<uint8_t>(c * out_b + (1.0f - c) * in_b);
        b[3] = 255;  // alpha
        b += 4;
      }
    }

    glGetError();  // Clear any error.
    BindTexture_(GL_TEXTURE_2D, vignette_tex_);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, data);

    // If 32 bit failed for some reason, attempt 16.
    GLenum err = glGetError();
    if (err != GL_NO_ERROR) {
      static bool reported = false;
      if (!reported) {
        Log(LogLevel::kError,
            "32-bit vignette creation failed; falling back to 16.");
        reported = true;
      }
      const int kVignetteTexWidth = 64;
      const int kVignetteTexHeight = 32;
      const int kVignetteTexBufferSize = kVignetteTexWidth * kVignetteTexHeight;
      uint16_t data2[kVignetteTexBufferSize];
      float max_c2 = 0.5f * 0.5f * 0.5f * 0.5f;
      uint16_t* b2 = data2;

      float out_r2 = std::min(
          32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_outer_r_)));
      float out_g2 = std::min(
          64.0f, std::max(0.0f, 64.0f * (1.0f - vignette_tex_outer_g_)));
      float out_b2 = std::min(
          32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_outer_b_)));
      float in_r2 = std::min(
          32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_inner_r_)));
      float in_g2 = std::min(
          64.0f, std::max(0.0f, 64.0f * (1.0f - vignette_tex_inner_g_)));
      float in_b2 = std::min(
          32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_inner_b_)));

      // IMPORTANT - if we tweak anything here we need to tweak vertex
      // shaders that calc this on the fly as well..
      for (int y = 0; y < height; y++) {
        float d3 = static_cast<float>(y) / (height - 1);
        float d4 = 1.0f - d3;
        for (int x = 0; x < width; x++) {
          float d1 = static_cast<float>(x) / (width - 1);
          float d2 = 1.0f - d1;
          float c = 1.0f * (1.0f - ((d1 * d2 * d3 * d4) / max_c2));
          c = 0.5f * (c * c) + 0.5f * c;
          c = std::min(1.0f, std::max(0.0f, c));
          int red =
              std::min(31, static_cast<int>(c * out_r2 + (1.0f - c) * in_r2));
          int green =
              std::min(63, static_cast<int>(c * out_g2 + (1.0f - c) * in_g2));
          int blue =
              std::min(31, static_cast<int>(c * out_b2 + (1.0f - c) * in_b2));
          *b2 = static_cast<uint16_t>(red << 11 | green << 5 | blue);
          b2 += 1;
        }
      }
      BindTexture_(GL_TEXTURE_2D, vignette_tex_);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
                   GL_UNSIGNED_SHORT_5_6_5, data2);
      BA_DEBUG_CHECK_GL_ERROR;
    }
    if (force) {
      BA_GL_LABEL_OBJECT(GL_TEXTURE, vignette_tex_, "vignetteTex");
    }
  }
}

auto RendererGL::GetFunkyDepthIssue_() -> bool {
  if (!funky_depth_issue_set_) {
    BA_LOG_ONCE(LogLevel::kError, "fetching funky depth issue but not set");
  }
  return funky_depth_issue_;
}

// auto RendererGL::GetDrawsShieldsFunny_() -> bool {
//   if (!draws_shields_funny_set_) {
//     BA_LOG_ONCE(LogLevel::kError,
//                 "fetching draws-shields-funny value but not set");
//   }
//   return draws_shields_funny_;
// }

#if BA_OSTYPE_ANDROID
std::string RendererGL::GetAutoAndroidRes() {
  assert(g_base->app_adapter->InGraphicsContext());

  // Simplifying this to just 1080p for anything we label 'speedy' and 720p
  // for everything else.
  if (is_speedy_android_device_) {
    return "1080p";
  }
  return "720p";
}
#endif  // BA_OSTYPE_ANDROID

auto RendererGL::GetAutoTextureQuality() -> TextureQuality {
  assert(g_base->app_adapter->InGraphicsContext());

  TextureQuality qual{TextureQuality::kHigh};

#if BA_OSTYPE_ANDROID
  {
    // Lets be cheaper in VR mode since we have to draw twice.
    if (g_core->vr_mode()) {
      qual = TextureQuality::kHigh;
    } else {
      qual = TextureQuality::kHigh;
    }
  }
#else  // BA_OSTYPE_ANDROID

  // On other platforms (iOS, mac, pc, etc) just default to high.
  qual = TextureQuality::kHigh;

#endif  // BA_OSTYPE_ANDROID

  return qual;
}

auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality {
  assert(g_base->app_adapter->InGraphicsContext());
  GraphicsQuality q{GraphicsQuality::kMedium};
#if BA_OSTYPE_ANDROID
  // lets be cheaper in VR mode since we draw twice..
  if (g_core->vr_mode()) {
    q = GraphicsQuality::kMedium;
  } else {
    if (is_speedy_android_device_) {
      q = GraphicsQuality::kHigher;
    } else {
      q = GraphicsQuality::kHigh;
    }
  }
#else
  // Elsewhere just assume we're working with something speedy.
  q = GraphicsQuality::kHigher;
#endif
  return q;
}

void RendererGL::RetainShader_(ProgramGL* p) { shaders_.emplace_back(p); }

void RendererGL::Load() {
  assert(g_base->app_adapter->InGraphicsContext());
  assert(!data_loaded_);
  assert(g_base->graphics_server->graphics_quality()
         != GraphicsQuality::kUnset);
  BA_DEBUG_CHECK_GL_ERROR;
  if (!got_screen_framebuffer_) {
    got_screen_framebuffer_ = true;

    // Grab the current framebuffer and consider that to be our 'screen'
    // framebuffer. This can be 0 for the main framebuffer or can be
    // something else.
    screen_framebuffer_ = GLGetInt(GL_FRAMEBUFFER_BINDING);
  }
  Renderer::Load();
  int high_qual_pp_flag =
      g_base->graphics_server->quality() >= GraphicsQuality::kHigher
          ? SHD_HIGHER_QUALITY
          : 0;
  screen_mesh_ = std::make_unique<MeshDataSimpleFullGL>(this);
  VertexSimpleFull v[] = {{{-1, -1, 0}, {0, 0}},
                          {{1, -1, 0}, {65535, 0}},
                          {{1, 1, 0}, {65535, 65535}},
                          {{-1, 1, 0}, {0, 65535}}};
  const uint16_t indices[] = {0, 1, 2, 0, 2, 3};
  MeshBuffer<VertexSimpleFull> buffer(4, v);
  buffer.state = 1;  // Necessary for this to set properly.
  MeshIndexBuffer16 i_buffer(6, indices);
  i_buffer.state = 1;  // Necessary for this to set properly.
  screen_mesh_->SetData(&buffer);
  screen_mesh_->SetIndexData(&i_buffer);
  assert(shaders_.empty());
  BA_DEBUG_CHECK_GL_ERROR;
  ProgramGL* p;
  p = simple_color_prog_ = new ProgramSimpleGL(this, SHD_MODULATE);
  RetainShader_(p);
  p = simple_tex_prog_ = new ProgramSimpleGL(this, SHD_TEXTURE);
  RetainShader_(p);
  p = simple_tex_dtest_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_DEPTH_BUG_TEST);
  RetainShader_(p);

  // Have to run this after we've created the shader to be able to test it.
  CheckFunkyDepthIssue_();
  p = simple_tex_mod_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE);
  RetainShader_(p);
  p = simple_tex_mod_flatness_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_FLATNESS);
  RetainShader_(p);
  p = simple_tex_mod_shadow_prog_ = new ProgramSimpleGL(
      this, SHD_TEXTURE | SHD_MODULATE | SHD_SHADOW | SHD_MASK_UV2);
  RetainShader_(p);
  p = simple_tex_mod_shadow_flatness_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_SHADOW
                                    | SHD_MASK_UV2 | SHD_FLATNESS);
  RetainShader_(p);
  p = simple_tex_mod_glow_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_GLOW);
  RetainShader_(p);
  p = simple_tex_mod_glow_maskuv2_prog_ = new ProgramSimpleGL(
      this, SHD_TEXTURE | SHD_MODULATE | SHD_GLOW | SHD_MASK_UV2);
  RetainShader_(p);
  p = simple_tex_mod_colorized_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE);
  RetainShader_(p);
  p = simple_tex_mod_colorized2_prog_ = new ProgramSimpleGL(
      this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE | SHD_COLORIZE2);
  RetainShader_(p);
  p = simple_tex_mod_colorized2_masked_prog_ =
      new ProgramSimpleGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE
                                    | SHD_COLORIZE2 | SHD_MASKED);
  RetainShader_(p);
  p = obj_prog_ = new ProgramObjectGL(this, 0);
  RetainShader_(p);
  p = obj_transparent_prog_ = new ProgramObjectGL(this, SHD_OBJ_TRANSPARENT);
  RetainShader_(p);
  p = obj_lightshad_transparent_prog_ =
      new ProgramObjectGL(this, SHD_OBJ_TRANSPARENT | SHD_LIGHT_SHADOW);
  RetainShader_(p);
  p = obj_refl_prog_ = new ProgramObjectGL(this, SHD_REFLECTION);
  RetainShader_(p);
  p = obj_refl_worldspace_prog_ =
      new ProgramObjectGL(this, SHD_REFLECTION | SHD_WORLD_SPACE_PTS);
  RetainShader_(p);
  p = obj_refl_transparent_prog_ =
      new ProgramObjectGL(this, SHD_REFLECTION | SHD_OBJ_TRANSPARENT);
  RetainShader_(p);
  p = obj_refl_add_transparent_prog_ =
      new ProgramObjectGL(this, SHD_REFLECTION | SHD_ADD | SHD_OBJ_TRANSPARENT);
  RetainShader_(p);
  p = obj_lightshad_prog_ = new ProgramObjectGL(this, SHD_LIGHT_SHADOW);
  RetainShader_(p);
  p = obj_lightshad_worldspace_prog_ =
      new ProgramObjectGL(this, SHD_LIGHT_SHADOW | SHD_WORLD_SPACE_PTS);
  RetainShader_(p);
  p = obj_refl_lightshad_prog_ =
      new ProgramObjectGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION);
  RetainShader_(p);
  p = obj_refl_lightshad_worldspace_prog_ = new ProgramObjectGL(
      this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_WORLD_SPACE_PTS);
  RetainShader_(p);
  p = obj_refl_lightshad_colorize_prog_ = new ProgramObjectGL(
      this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_COLORIZE);
  RetainShader_(p);
  p = obj_refl_lightshad_colorize2_prog_ = new ProgramObjectGL(
      this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_COLORIZE | SHD_COLORIZE2);
  RetainShader_(p);
  p = obj_refl_lightshad_add_prog_ =
      new ProgramObjectGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD);
  RetainShader_(p);
  p = obj_refl_lightshad_add_colorize_prog_ = new ProgramObjectGL(
      this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD | SHD_COLORIZE);
  RetainShader_(p);
  p = obj_refl_lightshad_add_colorize2_prog_ =
      new ProgramObjectGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD
                                    | SHD_COLORIZE | SHD_COLORIZE2);
  RetainShader_(p);
  p = smoke_prog_ =
      new ProgramSmokeGL(this, SHD_OBJ_TRANSPARENT | SHD_WORLD_SPACE_PTS);
  RetainShader_(p);
  p = smoke_overlay_prog_ = new ProgramSmokeGL(
      this, SHD_OBJ_TRANSPARENT | SHD_WORLD_SPACE_PTS | SHD_OVERLAY);
  RetainShader_(p);
  p = sprite_prog_ = new ProgramSpriteGL(this, SHD_COLOR);
  RetainShader_(p);
  p = sprite_camalign_prog_ =
      new ProgramSpriteGL(this, SHD_CAMERA_ALIGNED | SHD_COLOR);
  RetainShader_(p);
  p = sprite_camalign_overlay_prog_ =
      new ProgramSpriteGL(this, SHD_CAMERA_ALIGNED | SHD_OVERLAY | SHD_COLOR);
  RetainShader_(p);
  p = blur_prog_ = new ProgramBlurGL(this, 0);
  RetainShader_(p);
  p = shield_prog_ = new ProgramShieldGL(this, 0);
  RetainShader_(p);

  // Conditional seems to be a *very* slight win on some architectures (A7), a
  // loss on some (A5) and a wash on some (Adreno 320).
  // Gonna wait before a clean win before turning it on.
  p = postprocess_prog_ = new ProgramPostProcessGL(this, high_qual_pp_flag);
  RetainShader_(p);
  if (g_base->graphics_server->quality() >= GraphicsQuality::kHigher) {
    p = postprocess_eyes_prog_ = new ProgramPostProcessGL(this, SHD_EYES);
    RetainShader_(p);
  } else {
    postprocess_eyes_prog_ = nullptr;
  }
  p = postprocess_distort_prog_ =
      new ProgramPostProcessGL(this, SHD_DISTORT | high_qual_pp_flag);
  RetainShader_(p);

  // Generate our random value texture.
  // TODO(ericf): move this to assets.
  {
    glGenTextures(1, &random_tex_);
    BindTexture_(GL_TEXTURE_2D, random_tex_);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    const int tex_buffer_size = 128 * 128 * 3;
    unsigned char data[tex_buffer_size];
    for (unsigned char& i : data) {
      i = static_cast<unsigned char>(rand());  // NOLINT
    }
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 128, 128, 0, GL_RGB,
                 GL_UNSIGNED_BYTE, data);
    BA_GL_LABEL_OBJECT(GL_TEXTURE, random_tex_, "randomTex");
  }

  // Generate our vignette tex.
  // TODO(ericf): move this to assets.
  {
    glGenTextures(1, &vignette_tex_);
    BindTexture_(GL_TEXTURE_2D, vignette_tex_);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    UpdateVignetteTex_(true);
  }

  // Let's pre-fill our recyclable mesh-datas list to reduce the need to make
  // more which could cause hitches.
  assert(recycle_mesh_datas_simple_split_.empty());
  for (int i = 0; i < 10; i++) {
    recycle_mesh_datas_simple_split_.push_back(new MeshDataSimpleSplitGL(this));
  }
  assert(recycle_mesh_datas_object_split_.empty());
  for (int i = 0; i < 10; i++) {
    recycle_mesh_datas_object_split_.push_back(new MeshDataObjectSplitGL(this));
  }
  assert(recycle_mesh_datas_simple_full_.empty());
  for (int i = 0; i < 10; i++) {
    recycle_mesh_datas_simple_full_.push_back(new MeshDataSimpleFullGL(this));
  }
  assert(recycle_mesh_datas_dual_texture_full_.empty());
  for (int i = 0; i < 10; i++) {
    recycle_mesh_datas_dual_texture_full_.push_back(
        new MeshDataDualTextureFullGL(this));
  }
  assert(recycle_mesh_datas_smoke_full_.empty());
  for (int i = 0; i < 2; i++) {
    recycle_mesh_datas_smoke_full_.push_back(new MeshDataSmokeFullGL(this));
  }
  assert(recycle_mesh_datas_sprite_.empty());
  for (int i = 0; i < 2; i++) {
    recycle_mesh_datas_sprite_.push_back(new MeshDataSpriteGL(this));
  }

  // Re-sync with the GL state since we might be dealing with a new
  // context/etc.
  SyncGLState_();
  BA_DEBUG_CHECK_GL_ERROR;
  data_loaded_ = true;
}

void RendererGL::PostLoad() {
  Renderer::PostLoad();
  // Control may pass back to cardboard after we've finished loading but
  // before we render, (in cases such as graphics settings switches) ...and
  // it seems they can screw up our VAOs if we leave them bound. So lets be
  // defensive.
#if BA_CARDBOARD_BUILD
  SyncGLState_();
#endif
}

void RendererGL::Unload() {
  assert(g_base->app_adapter->InGraphicsContext());
  BA_DEBUG_CHECK_GL_ERROR;
  assert(data_loaded_);
  Renderer::Unload();
  // clear out recycle-mesh-datas
  for (auto&& i : recycle_mesh_datas_simple_split_) {
    delete i;
  }
  recycle_mesh_datas_simple_split_.clear();
  for (auto&& i : recycle_mesh_datas_object_split_) {
    delete i;
  }
  recycle_mesh_datas_object_split_.clear();
  for (auto&& i : recycle_mesh_datas_simple_full_) {
    delete i;
  }
  recycle_mesh_datas_simple_full_.clear();
  for (auto&& i : recycle_mesh_datas_dual_texture_full_) {
    delete i;
  }
  recycle_mesh_datas_dual_texture_full_.clear();
  for (auto&& i : recycle_mesh_datas_smoke_full_) {
    delete i;
  }
  recycle_mesh_datas_smoke_full_.clear();
  for (auto&& i : recycle_mesh_datas_sprite_) {
    delete i;
  }
  recycle_mesh_datas_sprite_.clear();
  screen_mesh_.reset();
  if (!g_base->graphics_server->renderer_context_lost()) {
    glDeleteTextures(1, &random_tex_);
    glDeleteTextures(1, &vignette_tex_);
  }
  blur_buffers_.clear();
  shaders_.clear();
  simple_color_prog_ = nullptr;
  simple_tex_prog_ = nullptr;
  simple_tex_dtest_prog_ = nullptr;
  simple_tex_mod_prog_ = nullptr;
  simple_tex_mod_flatness_prog_ = nullptr;
  simple_tex_mod_shadow_prog_ = nullptr;
  simple_tex_mod_shadow_flatness_prog_ = nullptr;
  simple_tex_mod_glow_prog_ = nullptr;
  simple_tex_mod_glow_maskuv2_prog_ = nullptr;
  simple_tex_mod_colorized_prog_ = nullptr;
  simple_tex_mod_colorized2_prog_ = nullptr;
  simple_tex_mod_colorized2_masked_prog_ = nullptr;
  obj_prog_ = nullptr;
  obj_transparent_prog_ = nullptr;
  obj_refl_prog_ = nullptr;
  obj_refl_worldspace_prog_ = nullptr;
  obj_refl_transparent_prog_ = nullptr;
  obj_refl_add_transparent_prog_ = nullptr;
  obj_lightshad_prog_ = nullptr;
  obj_lightshad_worldspace_prog_ = nullptr;
  obj_refl_lightshad_prog_ = nullptr;
  obj_refl_lightshad_worldspace_prog_ = nullptr;
  obj_refl_lightshad_colorize_prog_ = nullptr;
  obj_refl_lightshad_colorize2_prog_ = nullptr;
  obj_refl_lightshad_add_prog_ = nullptr;
  obj_refl_lightshad_add_colorize_prog_ = nullptr;
  obj_refl_lightshad_add_colorize2_prog_ = nullptr;
  smoke_prog_ = nullptr;
  smoke_overlay_prog_ = nullptr;
  sprite_prog_ = nullptr;
  sprite_camalign_prog_ = nullptr;
  sprite_camalign_overlay_prog_ = nullptr;
  obj_lightshad_transparent_prog_ = nullptr;
  blur_prog_ = nullptr;
  shield_prog_ = nullptr;
  postprocess_prog_ = nullptr;
  postprocess_eyes_prog_ = nullptr;
  postprocess_distort_prog_ = nullptr;
  data_loaded_ = false;
  BA_DEBUG_CHECK_GL_ERROR;
}

auto RendererGL::NewMeshAssetData(const MeshAsset& model)
    -> Object::Ref<MeshAssetRendererData> {
  return Object::New<MeshAssetRendererData, MeshAssetDataGL>(model, this);
}

auto RendererGL::NewTextureData(const TextureAsset& texture)
    -> Object::Ref<TextureAssetRendererData> {
  return Object::New<TextureAssetRendererData, TextureDataGL>(texture, this);
}

auto RendererGL::NewScreenRenderTarget() -> RenderTarget* {
  return Object::NewDeferred<RenderTargetGL>(this);
}

auto RendererGL::NewFramebufferRenderTarget(
    int width, int height, bool linear_interp, bool depth, bool texture,
    bool depth_texture, bool high_quality, bool msaa,
    bool alpha) -> Object::Ref<RenderTarget> {
  return Object::New<RenderTarget, RenderTargetGL>(
      this, width, height, linear_interp, depth, texture, depth_texture,
      high_quality, msaa, alpha);
}

auto RendererGL::NewMeshData(MeshDataType mesh_type,
                             MeshDrawType draw_type) -> MeshRendererData* {
  switch (mesh_type) {
    case MeshDataType::kIndexedSimpleSplit: {
      MeshDataSimpleSplitGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_simple_split_.rbegin();
      if (i != recycle_mesh_datas_simple_split_.rend()) {
        data = *i;
        recycle_mesh_datas_simple_split_.pop_back();
      } else {
        data = new MeshDataSimpleSplitGL(this);
      }
      return data;
      break;
    }
    case MeshDataType::kIndexedObjectSplit: {
      MeshDataObjectSplitGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_object_split_.rbegin();
      if (i != recycle_mesh_datas_object_split_.rend()) {
        data = *i;
        recycle_mesh_datas_object_split_.pop_back();
      } else {
        data = new MeshDataObjectSplitGL(this);
      }
      return data;
      break;
    }
    case MeshDataType::kIndexedSimpleFull: {
      MeshDataSimpleFullGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_simple_full_.rbegin();
      if (i != recycle_mesh_datas_simple_full_.rend()) {
        data = *i;
        recycle_mesh_datas_simple_full_.pop_back();
      } else {
        data = new MeshDataSimpleFullGL(this);
      }
      data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic);
      return data;
      break;
    }
    case MeshDataType::kIndexedDualTextureFull: {
      MeshDataDualTextureFullGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_dual_texture_full_.rbegin();
      if (i != recycle_mesh_datas_dual_texture_full_.rend()) {
        data = *i;
        recycle_mesh_datas_dual_texture_full_.pop_back();
      } else {
        data = new MeshDataDualTextureFullGL(this);
      }
      data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic);
      return data;
      break;
    }
    case MeshDataType::kIndexedSmokeFull: {
      MeshDataSmokeFullGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_smoke_full_.rbegin();
      if (i != recycle_mesh_datas_smoke_full_.rend()) {
        data = *i;
        recycle_mesh_datas_smoke_full_.pop_back();
      } else {
        data = new MeshDataSmokeFullGL(this);
      }
      data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic);
      return data;
      break;
    }
    case MeshDataType::kSprite: {
      MeshDataSpriteGL* data;
      // use a recycled one if we've got one.. otherwise create a new one
      auto i = recycle_mesh_datas_sprite_.rbegin();
      if (i != recycle_mesh_datas_sprite_.rend()) {
        data = *i;
        recycle_mesh_datas_sprite_.pop_back();
      } else {
        data = new MeshDataSpriteGL(this);
      }
      data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic);
      return data;
      break;
    }
    default:
      throw Exception();
      break;
  }
}

void RendererGL::DeleteMeshData(MeshRendererData* source_in,
                                MeshDataType mesh_type) {
  // When we're done with mesh-data we keep it around for recycling.
  // It seems that killing off VAO/VBOs can be hitchy (on mac at least).
  // Hmmm should we have some sort of threshold at which point we kill off
  // some?

  switch (mesh_type) {
    case MeshDataType::kIndexedSimpleSplit: {
      auto source = static_cast<MeshDataSimpleSplitGL*>(source_in);
      assert(source
             && source == dynamic_cast<MeshDataSimpleSplitGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_simple_split_.push_back(source);
      break;
    }
    case MeshDataType::kIndexedObjectSplit: {
      auto source = static_cast<MeshDataObjectSplitGL*>(source_in);
      assert(source
             && source == dynamic_cast<MeshDataObjectSplitGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_object_split_.push_back(source);
      break;
    }
    case MeshDataType::kIndexedSimpleFull: {
      auto source = static_cast<MeshDataSimpleFullGL*>(source_in);
      assert(source
             && source == dynamic_cast<MeshDataSimpleFullGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_simple_full_.push_back(source);
      break;
    }
    case MeshDataType::kIndexedDualTextureFull: {
      auto source = static_cast<MeshDataDualTextureFullGL*>(source_in);
      assert(source
             && source == dynamic_cast<MeshDataDualTextureFullGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_dual_texture_full_.push_back(source);
      break;
    }
    case MeshDataType::kIndexedSmokeFull: {
      auto source = static_cast<MeshDataSmokeFullGL*>(source_in);
      assert(source && source == dynamic_cast<MeshDataSmokeFullGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_smoke_full_.push_back(source);
      break;
    }
    case MeshDataType::kSprite: {
      auto source = static_cast<MeshDataSpriteGL*>(source_in);
      assert(source && source == dynamic_cast<MeshDataSpriteGL*>(source_in));
      source->Reset();
      recycle_mesh_datas_sprite_.push_back(source);
      break;
    }
    default:
      throw Exception();
      break;
  }
}

void RendererGL::CheckForErrors() {
  // lets only check periodically.. i doubt it hurts to run this all the time
  // but just in case...
  error_check_counter_++;
  if (error_check_counter_ > 120) {
    error_check_counter_ = 0;
    BA_CHECK_GL_ERROR;
  }
}

void RendererGL::DrawDebug() {
  if (explicit_bool(false)) {
    // Draw our cam buffer if we have it.
    if (has_camera_render_target()) {
      SetDepthWriting(false);
      SetDepthTesting(false);
      SetDoubleSided_(false);
      SetBlend(false);
      ProgramSimpleGL* p = simple_tex_prog_;
      p->Bind();

      g_base->graphics_server->ModelViewReset();
      g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1);

      float tx = -0.6f;
      float ty = 0.6f;

      g_base->graphics_server->PushTransform();
      g_base->graphics_server->scale(Vector3f(0.4f, 0.4f, 0.4f));
      g_base->graphics_server->Translate(Vector3f(-1.3f, -0.7f, 0));

      // Draw cam buffer.
      g_base->graphics_server->PushTransform();
      g_base->graphics_server->Translate(Vector3f(tx, ty, 0));
      tx += 0.2f;
      ty -= 0.25f;
      g_base->graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f));
      p->SetColorTexture(static_cast<RenderTargetGL*>(camera_render_target())
                             ->framebuffer()
                             ->texture());
      GetActiveProgram_()->PrepareToDraw();
      screen_mesh_->Bind();
      screen_mesh_->Draw(DrawType::kTriangles);
      g_base->graphics_server->PopTransform();

      // Draw blur buffers.
      if (explicit_bool(false)) {
        for (auto&& i : blur_buffers_) {
          g_base->graphics_server->PushTransform();
          g_base->graphics_server->Translate(Vector3f(tx, ty, 0));
          tx += 0.2f;
          ty -= 0.25f;
          g_base->graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f));
          p->SetColorTexture(i->texture());
          GetActiveProgram_()->PrepareToDraw();
          screen_mesh_->Bind();
          screen_mesh_->Draw(DrawType::kTriangles);
          g_base->graphics_server->PopTransform();
        }
      }
      g_base->graphics_server->PopTransform();
    }
  }
}

void RendererGL::GenerateCameraBufferBlurPasses() {
  // If our cam-buffer res has changed since last time, regenerate our blur
  // buffers.
  auto* cam_buffer = static_cast<RenderTargetGL*>(camera_render_target());
  assert(cam_buffer != nullptr
         && dynamic_cast<RenderTargetGL*>(camera_render_target())
                == cam_buffer);

  if (cam_buffer->physical_width() != last_cam_buffer_width_
      || cam_buffer->physical_height() != last_cam_buffer_height_
      || blur_res_count() != last_blur_res_count_ || blur_buffers_.empty()) {
    blur_buffers_.clear();
    last_cam_buffer_width_ = cam_buffer->physical_width();
    last_cam_buffer_height_ = cam_buffer->physical_height();
    last_blur_res_count_ = blur_res_count();
    int w = static_cast<int>(last_cam_buffer_width_);
    int h = static_cast<int>(last_cam_buffer_height_);

    // In higher-quality we do multiple levels and 16-bit dithering is kinda
    // noticeable and ugly then.
    bool high_quality_fbos =
        (g_base->graphics_server->quality() >= GraphicsQuality::kHigher);
    for (int i = 0; i < blur_res_count(); i++) {
      assert(w % 2 == 0);
      assert(h % 2 == 0);
      w /= 2;
      h /= 2;
      blur_buffers_.push_back(Object::New<FramebufferObjectGL>(
          this, w, h,
          true,               // linear_interp
          false,              // depth
          true,               // tex
          false,              // depthTex
          high_quality_fbos,  // highQuality
          false,              // msaa
          false               // alpha
          ));                 // NOLINT(whitespace/parens)
    }

    // Final redundant one (we run an extra blur without down-rezing).
    if (g_base->graphics_server->quality() >= GraphicsQuality::kHigher)
      blur_buffers_.push_back(Object::New<FramebufferObjectGL>(
          this, w, h,
          true,   // linear_interp
          false,  // depth
          true,   // tex
          false,  // depthTex
          false,  // highQuality
          false,  // msaa
          false   // alpha
          ));     // NOLINT(whitespace/parens)
  }

  // Ok now go through and do the blurring.
  SetDepthWriting(false);
  SetDepthTesting(false);
  g_base->graphics_server->ModelViewReset();
  g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1);
  SetDoubleSided_(false);
  SetBlend(false);

  ProgramBlurGL* p = blur_prog_;
  p->Bind();

  FramebufferObjectGL* src_fb =
      static_cast<RenderTargetGL*>(camera_render_target())->framebuffer();
  for (auto&& i : blur_buffers_) {
    FramebufferObjectGL* fb = i.Get();
    assert(fb);
    fb->Bind();
    SetViewport_(0, 0, fb->width(), fb->height());
    InvalidateFramebuffer(true, false, false);
    p->SetColorTexture(src_fb->texture());
    if (fb->width() == src_fb->width()) {  // Our last one is equal res.
      p->SetPixelSize(2.0f / static_cast<float>(fb->width()),
                      2.0f / static_cast<float>(fb->height()));
    } else {
      p->SetPixelSize(1.0f / static_cast<float>(fb->width()),
                      1.0f / static_cast<float>(fb->height()));
    }
    GetActiveProgram_()->PrepareToDraw();
    screen_mesh_->Bind();
    screen_mesh_->Draw(DrawType::kTriangles);
    src_fb = fb;
  }
}

void RendererGL::CardboardDisableScissor() { glDisable(GL_SCISSOR_TEST); }

void RendererGL::CardboardEnableScissor() { glEnable(GL_SCISSOR_TEST); }

void RendererGL::VREyeRenderBegin() {
  assert(g_core->vr_mode());

  // On rift we need to turn off srgb conversion for each eye render
  // so we can dump our linear data into oculus' srgb buffer as-is.
  // (we really should add proper srgb support to the engine at some point)
#if BA_RIFT_BUILD
  glDisable(GL_FRAMEBUFFER_SRGB);
#endif  // BA_RIFT_BUILD

  screen_framebuffer_ = GLGetInt(GL_FRAMEBUFFER_BINDING);
}

#if BA_VR_BUILD
void RendererGL::VRSyncRenderStates() {
  // GL state has been mucked with outside of our code; let's resync stuff..
  SyncGLState_();
}
#endif  // BA_VR_BUILD

void RendererGL::RenderFrameDefEnd() {
  // Need to set some states to keep cardboard happy.
#if BA_CARDBOARD_BUILD
  if (g_core->vr_mode()) {
    SyncGLState_();
    glEnable(GL_SCISSOR_TEST);
  }
#endif  // BA_CARDBOARD_BUILD
}

}  // namespace ballistica::base

#endif  // BA_ENABLE_OPENGL
