#include <SDL2/SDL.h>
#define BITMAP WINDOWS_BITMAP
#include <SDL2/SDL_opengl.h>
#if SDL_VERSION_ATLEAST(2, 0, 4)
#include <SDL2/SDL_opengl_glext.h>
#endif
#undef BITMAP
#include "wx-sdl2-glw.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "video.h"
#include "wx-sdl2-video.h"
#include "wx-sdl2-video-renderer.h"
#include "ibm.h"
#include "wx-glslp-parser.h"
#include "wx-utils.h"
#include "config.h"
#include "wx-glsl.h"

extern char *get_filename(char *);

extern int take_screenshot;
extern void screenshot_taken(unsigned char *rgb, int width, int height);

#define SCALE_SOURCE 0
#define SCALE_VIEWPORT 1
#define SCALE_ABSOLUTE 2

float gl3_shader_refresh_rate = 0;
float gl3_input_scale = 1.0f;
int gl3_input_stretch = FULLSCR_SCALE_FULL;
char gl3_shader_file[MAX_USER_SHADERS][512];

static int max_texture_size;

static SDL_GLContext context = NULL;
static struct shader_texture scene_texture;

static glsl_t *active_shader;

static glw_t *glw;

static GLfloat matrix[] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};

extern int video_scale_mode;
extern int video_vsync;
extern int video_focus_dim;
extern int video_refresh_rate;

static int glsl_version[2];

const char *vertex_shader_default_tex_src = "#version 130\n"
                                            "\n"
                                            "in vec4 VertexCoord;\n"
                                            "in vec2 TexCoord;\n"
                                            "\n"
                                            "out vec2 texCoord;\n"
                                            "\n"
                                            "void main()\n"
                                            "{\n"
                                            "       gl_Position = VertexCoord;\n"
                                            "       texCoord = TexCoord;\n"
                                            "}\n";

const char *fragment_shader_default_tex_src = "#version 130\n"
                                              "\n"
                                              "in vec2 texCoord;\n"
                                              "uniform sampler2D Texture;\n"
                                              "\n"
                                              "out vec4 color;"
                                              "\n"
                                              "void main()\n"
                                              "{\n"
                                              "       color = texture(Texture, texCoord);\n"
                                              "}\n";

const char *vertex_shader_default_color_src = "#version 130\n"
                                              "\n"
                                              "in vec4 VertexCoord;\n"
                                              "in vec4 Color;\n"
                                              "\n"
                                              "out vec4 color;\n"
                                              "\n"
                                              "void main()\n"
                                              "{\n"
                                              "       gl_Position = VertexCoord;\n"
                                              "       color = Color;\n"
                                              "}\n";

const char *fragment_shader_default_color_src = "#version 130\n"
                                                "\n"
                                                "in vec4 color;\n"
                                                "\n"
                                                "out vec4 outColor;"
                                                "\n"
                                                "void main()\n"
                                                "{\n"
                                                "       outColor = color;\n"
                                                "}\n";

static int next_pow2(unsigned int n) {
        n--;
        n |= n >> 1; // Divide by 2^k for consecutive doublings of k up to 32,
        n |= n >> 2; // and then or the results.
        n |= n >> 4;
        n |= n >> 8;
        n |= n >> 16;
        n++;

        return n;
}

static int compile_shader(GLenum shader_type, const char *prepend, const char *program, int *dst) {
        const char *source[3];
        char version[50];
        int ver = 0;
        char *version_loc = strstr(program, "#version");
        if (version_loc)
                ver = (int)strtol(version_loc + 8, (char **)&program, 10);
        else {
                ver = glsl_version[0] * 100 + glsl_version[1] * 10;
                if (ver == 300)
                        ver = 130;
                else if (ver == 310)
                        ver = 140;
                else if (ver == 320)
                        ver = 150;
        }
        sprintf(version, "#version %d\n", ver);
        source[0] = version;
        source[1] = prepend ? prepend : "";
        source[2] = program;

        pclog("GLSL version %d\n", ver);

        GLuint shader = glw->glCreateShader(shader_type);
        glw->glShaderSource(shader, 3, source, NULL);
        glw->glCompileShader(shader);

        GLint status = 0;
        glw->glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
        if (!status) {
                GLint length;
                glw->glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
                char *log = malloc(length);
                glw->glGetShaderInfoLog(shader, length, &length, log);
                wx_simple_messagebox("GLSL Error", "Could not compile shader:\n%s", log);
                pclog("Could not compile shader: %s\n", log);
                //                pclog("Shader: %s\n", program);

                free(log);
                return 0;
        }

        *dst = shader;

        return 1;
}

static int create_program(struct shader_program *program) {
        GLint status;
        program->id = glw->glCreateProgram();
        glw->glAttachShader(program->id, program->vertex_shader);
        glw->glAttachShader(program->id, program->fragment_shader);

        glw->glLinkProgram(program->id);

        glw->glDeleteShader(program->vertex_shader);
        glw->glDeleteShader(program->fragment_shader);

        program->vertex_shader = program->fragment_shader = 0;

        glw->glGetProgramiv(program->id, GL_LINK_STATUS, &status);

        if (!status) {
                int maxLength;
                int length;
                glw->glGetProgramiv(program->id, GL_INFO_LOG_LENGTH, &maxLength);
                char *log = malloc(maxLength);
                glw->glGetProgramInfoLog(program->id, maxLength, &length, log);
                wx_simple_messagebox("GLSL Error", "Program not linked:\n%s", log);
                free(log);
                return 0;
        }

        return 1;
}

static GLuint get_uniform(GLuint program, const char *name) { return glw->glGetUniformLocation(program, name); }

static GLuint get_attrib(GLuint program, const char *name) { return glw->glGetAttribLocation(program, name); }

static void find_uniforms(struct glsl_shader *glsl, int num_pass) {
        int i;
        char s[50];
        struct shader_pass *pass = &glsl->passes[num_pass];
        int p = pass->program.id;
        glw->glUseProgram(p);

        struct shader_uniforms *u = &pass->uniforms;

        u->mvp_matrix = get_uniform(p, "MVPMatrix");
        u->vertex_coord = get_attrib(p, "VertexCoord");
        u->tex_coord = get_attrib(p, "TexCoord");
        u->color = get_attrib(p, "Color");

        u->frame_count = get_uniform(p, "FrameCount");
        u->frame_direction = get_uniform(p, "FrameDirection");

        u->texture = get_uniform(p, "Texture");
        u->input_size = get_uniform(p, "InputSize");
        u->texture_size = get_uniform(p, "TextureSize");
        u->output_size = get_uniform(p, "OutputSize");

        u->orig.texture = get_uniform(p, "OrigTexture");
        u->orig.input_size = get_uniform(p, "OrigInputSize");
        u->orig.texture_size = get_uniform(p, "OrigTextureSize");

        for (i = 0; i < glsl->num_passes; ++i) {
                sprintf(s, "Pass%dTexture", (i + 1));
                u->pass[i].texture = get_uniform(p, s);
                sprintf(s, "Pass%dInputSize", (i + 1));
                u->pass[i].input_size = get_uniform(p, s);
                sprintf(s, "Pass%dTextureSize", (i + 1));
                u->pass[i].texture_size = get_uniform(p, s);

                sprintf(s, "PassPrev%dTexture", num_pass - i);
                u->prev_pass[i].texture = get_uniform(p, s);
                sprintf(s, "PassPrev%dInputSize", num_pass - i);
                u->prev_pass[i].input_size = get_uniform(p, s);
                sprintf(s, "PassPrev%dTextureSize", num_pass - i);
                u->prev_pass[i].texture_size = get_uniform(p, s);
        }

        u->prev[0].texture = get_uniform(p, "PrevTexture");
        u->prev[0].tex_coord = get_attrib(p, "PrevTexCoord");
        for (i = 1; i < MAX_PREV; ++i) {
                sprintf(s, "Prev%dTexture", i);
                u->prev[i].texture = get_uniform(p, s);
                sprintf(s, "Prev%dTexCoord", i);
                u->prev[i].tex_coord = get_attrib(p, s);
        }
        for (i = 0; i < MAX_PREV; ++i)
                if (u->prev[i].texture >= 0)
                        glsl->has_prev = 1;

        for (i = 0; i < glsl->num_lut_textures; ++i)
                u->lut_textures[i] = get_uniform(p, glsl->lut_textures[i].name);

        for (i = 0; i < glsl->num_parameters; ++i)
                u->parameters[i] = get_uniform(p, glsl->parameters[i].id);

        glw->glUseProgram(0);
}

static void set_scale_mode(char *scale, int *dst) {
        if (!strcmp(scale, "viewport"))
                *dst = SCALE_VIEWPORT;
        else if (!strcmp(scale, "absolute"))
                *dst = SCALE_ABSOLUTE;
        else
                *dst = SCALE_SOURCE;
}

static void setup_scale(struct shader *shader, struct shader_pass *pass) {
        set_scale_mode(shader->scale_type_x, &pass->scale.mode[0]);
        set_scale_mode(shader->scale_type_y, &pass->scale.mode[1]);
        pass->scale.value[0] = shader->scale_x;
        pass->scale.value[1] = shader->scale_y;
}

static void create_texture(struct shader_texture *tex) {
        if (tex->width > max_texture_size)
                tex->width = max_texture_size;
        if (tex->height > max_texture_size)
                tex->height = max_texture_size;
        pclog("Create texture with size %dx%d\n", tex->width, tex->height);
        glGenTextures(1, (GLuint *)&tex->id);
        glBindTexture(GL_TEXTURE_2D, tex->id);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, tex->wrap_mode);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, tex->wrap_mode);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, tex->min_filter);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, tex->mag_filter);
        glTexImage2D(GL_TEXTURE_2D, 0, tex->internal_format, tex->width, tex->height, 0, tex->format, tex->type, tex->data);
        if (tex->mipmap)
                glw->glGenerateMipmap(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, 0);
}

static void delete_texture(struct shader_texture *tex) {
        if (tex->id > 0)
                glDeleteTextures(1, (GLuint *)&tex->id);
        tex->id = 0;
}

static void delete_fbo(struct shader_fbo *fbo) {
        if (fbo->id >= 0) {
                glw->glDeleteFramebuffers(1, (GLuint *)&fbo->id);
                delete_texture(&fbo->texture);
        }
}

static void delete_program(struct shader_program *program) {
        if (program->vertex_shader)
                glw->glDeleteShader(program->vertex_shader);
        if (program->fragment_shader)
                glw->glDeleteShader(program->fragment_shader);
        glw->glDeleteProgram(program->id);
}

static void delete_vbo(struct shader_vbo *vbo) {
        if (vbo->color >= 0)
                glw->glDeleteBuffers(1, (GLuint *)&vbo->color);
        glw->glDeleteBuffers(1, (GLuint *)&vbo->vertex_coord);
        glw->glDeleteBuffers(1, (GLuint *)&vbo->tex_coord);
}

static void delete_pass(struct shader_pass *pass) {
        delete_fbo(&pass->fbo);
        delete_vbo(&pass->vbo);
        delete_program(&pass->program);
        glw->glDeleteVertexArrays(1, (GLuint *)&pass->vertex_array);
}

static void delete_prev(struct shader_prev *prev) {
        delete_fbo(&prev->fbo);
        delete_vbo(&prev->vbo);
}

static void delete_shader(struct glsl_shader *glsl) {
        int i;
        for (i = 0; i < glsl->num_passes; ++i)
                delete_pass(&glsl->passes[i]);
        if (glsl->has_prev) {
                delete_pass(&glsl->prev_scene);
                for (i = 0; i < MAX_PREV; ++i)
                        delete_prev(&glsl->prev[i]);
        }
        for (i = 0; i < glsl->num_lut_textures; ++i)
                delete_texture(&glsl->lut_textures[i].texture);
}

static void delete_glsl(glsl_t *glsl) {
        int i;
        for (i = 0; i < glsl->num_shaders; ++i)
                delete_shader(&glsl->shaders[i]);
        delete_pass(&glsl->scene);
        delete_pass(&glsl->fs_color);
        delete_pass(&glsl->final_pass);
#ifdef SDL2_SHADER_DEBUG
        delete_pass(&glsl->debug);
#endif
}

static void create_fbo(struct shader_fbo *fbo) {
        create_texture(&fbo->texture);

        glw->glGenFramebuffers(1, (GLuint *)&fbo->id);
        glw->glBindFramebuffer(GL_FRAMEBUFFER, fbo->id);
        glw->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo->texture.id, 0);

        if (glw->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
                pclog("Could not create framebuffer!\n");

        glw->glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

static void setup_fbo(struct shader *shader, struct shader_fbo *fbo) {
        fbo->texture.internal_format = GL_RGBA8;
        fbo->texture.format = GL_RGBA;
        fbo->texture.min_filter = fbo->texture.mag_filter = shader->filter_linear ? GL_LINEAR : GL_NEAREST;
        fbo->texture.width = 1024;
        fbo->texture.height = 1024;
        fbo->texture.type = GL_UNSIGNED_BYTE;
        if (!strcmp(shader->wrap_mode, "repeat"))
                fbo->texture.wrap_mode = GL_REPEAT;
        else if (!strcmp(shader->wrap_mode, "mirrored_repeat"))
                fbo->texture.wrap_mode = GL_MIRRORED_REPEAT;
        else if (!strcmp(shader->wrap_mode, "clamp_to_edge"))
                fbo->texture.wrap_mode = GL_CLAMP_TO_EDGE;
        else
                fbo->texture.wrap_mode = GL_CLAMP_TO_BORDER;
        fbo->srgb = 0;
        if (shader->srgb_framebuffer) {
                fbo->texture.internal_format = GL_SRGB8_ALPHA8;
                fbo->srgb = 1;
        } else if (shader->float_framebuffer) {
                fbo->texture.internal_format = GL_RGBA32F;
                fbo->texture.type = GL_FLOAT;
        }

        if (fbo->texture.mipmap)
                fbo->texture.min_filter = shader->filter_linear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_NEAREST;

        create_fbo(fbo);
}

static void recreate_fbo(struct shader_fbo *fbo, int width, int height) {
        if (width != fbo->texture.width || height != fbo->texture.height) {
                glw->glDeleteFramebuffers(1, (GLuint *)&fbo->id);
                glDeleteTextures(1, (GLuint *)&fbo->texture.id);
                fbo->texture.width = width;
                fbo->texture.height = height;
                create_fbo(fbo);
        }
}

static int create_default_shader_tex(struct shader_pass *pass) {
        if (!compile_shader(GL_VERTEX_SHADER, 0, vertex_shader_default_tex_src, &pass->program.vertex_shader) ||
            !compile_shader(GL_FRAGMENT_SHADER, 0, fragment_shader_default_tex_src, &pass->program.fragment_shader) ||
            !create_program(&pass->program))
                return 0;
        glw->glGenVertexArrays(1, (GLuint *)&pass->vertex_array);

        struct shader_uniforms *u = &pass->uniforms;
        int p = pass->program.id;
        memset(u, -1, sizeof(struct shader_uniforms));
        u->vertex_coord = get_attrib(p, "VertexCoord");
        u->tex_coord = get_attrib(p, "TexCoord");
        u->texture = get_uniform(p, "Texture");
        pass->scale.mode[0] = pass->scale.mode[1] = SCALE_SOURCE;
        pass->scale.value[0] = pass->scale.value[1] = 1.0f;
        pass->fbo.id = -1;
        pass->active = 1;
        return 1;
}

static int create_default_shader_color(struct shader_pass *pass) {
        if (!compile_shader(GL_VERTEX_SHADER, 0, vertex_shader_default_color_src, &pass->program.vertex_shader) ||
            !compile_shader(GL_FRAGMENT_SHADER, 0, fragment_shader_default_color_src, &pass->program.fragment_shader) ||
            !create_program(&pass->program))
                return 0;
        glw->glGenVertexArrays(1, (GLuint *)&pass->vertex_array);

        struct shader_uniforms *u = &pass->uniforms;
        int p = pass->program.id;
        memset(u, -1, sizeof(struct shader_uniforms));
        u->vertex_coord = get_attrib(p, "VertexCoord");
        u->color = get_attrib(p, "Color");
        pass->scale.mode[0] = pass->scale.mode[1] = SCALE_SOURCE;
        pass->scale.value[0] = pass->scale.value[1] = 1.0f;
        pass->fbo.id = -1;
        pass->active = 1;
        return 1;
}

/* create the default scene shader */
static void create_scene_shader() {
        struct shader scene_shader_conf;
        memset(&scene_shader_conf, 0, sizeof(struct shader));
        create_default_shader_tex(&active_shader->scene);
        scene_shader_conf.filter_linear = video_scale_mode;
        if (active_shader->num_shaders > 0 && active_shader->shaders[0].input_filter_linear >= 0)
                scene_shader_conf.filter_linear = active_shader->shaders[0].input_filter_linear;
        setup_fbo(&scene_shader_conf, &active_shader->scene.fbo);

        memset(&scene_shader_conf, 0, sizeof(struct shader));
        create_default_shader_color(&active_shader->fs_color);
        setup_fbo(&scene_shader_conf, &active_shader->fs_color.fbo);
}

static int load_texture(const char *f, struct shader_texture *tex) {
        void *img = wx_image_load(f);
        if (!img)
                return 0;
        int width, height;
        wx_image_get_size(img, &width, &height);

        if (width != next_pow2(width) || height != next_pow2(height))
                wx_image_rescale(img, next_pow2(width), next_pow2(height));

        wx_image_get_size(img, &width, &height);

        GLubyte *rgb = wx_image_get_data(img);
        GLubyte *alpha = wx_image_get_alpha(img);

        int bpp = alpha ? 4 : 3;

        GLubyte *data = malloc(width * height * bpp);

        int x, y, Y;
        for (y = 0; y < height; ++y) {
                Y = height - y - 1;
                for (x = 0; x < width; x++) {
                        data[(y * width + x) * bpp + 0] = rgb[(Y * width + x) * 3 + 0];
                        data[(y * width + x) * bpp + 1] = rgb[(Y * width + x) * 3 + 1];
                        data[(y * width + x) * bpp + 2] = rgb[(Y * width + x) * 3 + 2];

                        if (alpha)
                                data[(y * width + x) * bpp + 3] = alpha[Y * width + x];
                }
        }
        wx_image_free(img);

        tex->width = width;
        tex->height = height;
        tex->internal_format = alpha ? GL_RGBA8 : GL_RGB8;
        tex->format = alpha ? GL_RGBA : GL_RGB;
        tex->type = GL_UNSIGNED_BYTE;
        tex->data = data;
        return 1;
}

static glsl_t *load_glslp(glsl_t *glsl, int num_shader, const char *f) {
        int i, j;
        glslp_t *p = glslp_parse(f);

        if (p) {
                char path[512];
                char file[1024];
                int failed = 0;
                strcpy(path, f);
                char *filename = get_filename(path);

                struct glsl_shader *gshader = &glsl->shaders[num_shader];

                strcpy(gshader->name, p->name);
                *filename = 0;

                gshader->num_lut_textures = p->num_textures;

                for (i = 0; i < p->num_textures; ++i) {
                        struct texture *texture = &p->textures[i];

                        sprintf(file, "%s%s", path, texture->path);

                        struct shader_lut_texture *tex = &gshader->lut_textures[i];
                        strcpy(tex->name, texture->name);

                        pclog("Load texture %s...\n", file);

                        if (!load_texture(file, &tex->texture)) {
                                wx_simple_messagebox("GLSL Error", "Could not load texture: %s", file);
                                pclog("Could not load texture %s!\n", file);
                                failed = 1;
                                break;
                        }

                        if (!strcmp(texture->wrap_mode, "repeat"))
                                tex->texture.wrap_mode = GL_REPEAT;
                        else if (!strcmp(texture->wrap_mode, "mirrored_repeat"))
                                tex->texture.wrap_mode = GL_MIRRORED_REPEAT;
                        else if (!strcmp(texture->wrap_mode, "clamp_to_edge"))
                                tex->texture.wrap_mode = GL_CLAMP_TO_EDGE;
                        else
                                tex->texture.wrap_mode = GL_CLAMP_TO_BORDER;

                        tex->texture.mipmap = texture->mipmap;

                        tex->texture.min_filter = tex->texture.mag_filter = texture->linear ? GL_LINEAR : GL_NEAREST;
                        if (tex->texture.mipmap)
                                tex->texture.min_filter = texture->linear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_NEAREST;

                        create_texture(&tex->texture);
                        free(tex->texture.data);
                        tex->texture.data = 0;
                }

                if (!failed) {
                        gshader->input_filter_linear = p->input_filter_linear;

                        gshader->num_parameters = p->num_parameters;
                        for (j = 0; j < gshader->num_parameters; ++j)
                                memcpy(&gshader->parameters[j], &p->parameters[j], sizeof(struct shader_parameter));

                        gshader->num_passes = p->num_shaders;

                        for (i = 0; i < p->num_shaders; ++i) {
                                struct shader *shader = &p->shaders[i];
                                struct shader_pass *pass = &gshader->passes[i];

                                strcpy(pass->alias, shader->alias);
                                if (!strlen(pass->alias))
                                        sprintf(pass->alias, "Pass %u", (i + 1));

                                pclog("Creating pass %u (%s)\n", (i + 1), pass->alias);
                                pclog("Loading shader %s...\n", shader->shader_fn);
                                if (!shader->shader_program) {
                                        wx_simple_messagebox("GLSL Error", "Could not load shader: %s", shader->shader_fn);
                                        pclog("Could not load shader %s\n", shader->shader_fn);
                                        failed = 1;
                                        break;
                                } else
                                        pclog("Shader %s loaded\n", shader->shader_fn);
                                failed = !compile_shader(GL_VERTEX_SHADER, "#define VERTEX\n#define PARAMETER_UNIFORM\n",
                                                         shader->shader_program, &pass->program.vertex_shader) ||
                                         !compile_shader(GL_FRAGMENT_SHADER, "#define FRAGMENT\n#define PARAMETER_UNIFORM\n",
                                                         shader->shader_program, &pass->program.fragment_shader);
                                if (failed)
                                        break;

                                if (!create_program(&pass->program)) {
                                        failed = 1;
                                        break;
                                }
                                pass->frame_count_mod = shader->frame_count_mod;
                                pass->fbo.mipmap_input = shader->mipmap_input;

                                glw->glGenVertexArrays(1, (GLuint *)&pass->vertex_array);
                                find_uniforms(gshader, i);
                                setup_scale(shader, pass);
                                if (i == p->num_shaders - 1) /* last pass may or may not be an fbo depending on scale */
                                {
                                        if (num_shader == glsl->num_shaders - 1) {
                                                pass->fbo.id = -1;

                                                for (j = 0; j < 2; ++j) {
                                                        if (pass->scale.mode[j] != SCALE_SOURCE || pass->scale.value[j] != 1) {
                                                                setup_fbo(shader, &pass->fbo);
                                                                break;
                                                        }
                                                }
                                        } else {
                                                /* check if next shaders' first pass wants the input mipmapped (will this ever
                                                 * happen?) */
                                                pass->fbo.texture.mipmap =
                                                        glsl->shaders[num_shader + 1].num_passes > 0 &&
                                                        glsl->shaders[num_shader + 1].passes[0].fbo.mipmap_input;
                                                /* check if next shader wants the output of this pass to be filtered */
                                                if (glsl->shaders[num_shader + 1].num_passes > 0 &&
                                                    glsl->shaders[num_shader + 1].input_filter_linear >= 0)
                                                        shader->filter_linear = glsl->shaders[num_shader + 1].input_filter_linear;
                                                setup_fbo(shader, &pass->fbo);
                                        }
                                } else {
                                        /* check if next pass wants the input mipmapped, if so we need to generate mipmaps of this
                                         * pass */
                                        pass->fbo.texture.mipmap = (i + 1) < p->num_shaders && p->shaders[i + 1].mipmap_input;
                                        setup_fbo(shader, &pass->fbo);
                                }
                                if (pass->fbo.srgb)
                                        glsl->srgb = 1;
                                pass->active = 1;
                        }
                        if (!failed) {
                                if (gshader->has_prev) {
                                        struct shader scene_shader_conf;
                                        memset(&scene_shader_conf, 0, sizeof(struct shader));
                                        for (i = 0; i < MAX_PREV; ++i) {
                                                setup_fbo(&scene_shader_conf, &gshader->prev[i].fbo);
                                        }
                                }
                        }
                }

                glslp_free(p);

                return glsl;
        }
        return 0;
}

static glsl_t *load_shaders(int num, char shaders[MAX_USER_SHADERS][512]) {
        int i;
        glsl_t *glsl;

        glsl = malloc(sizeof(glsl_t));
        memset(glsl, 0, sizeof(glsl_t));

        glsl->num_shaders = num;
        int failed = 0;
        for (i = num - 1; i >= 0; --i) {
                const char *f = shaders[i];
                if (f && strlen(f)) {
                        if (!load_glslp(glsl, i, f)) {
                                failed = 1;
                                break;
                        }
                }
        }
        if (failed) {
                delete_glsl(glsl);
                memset(glsl, 0, sizeof(glsl_t));
        }
        return glsl;
}

static void read_shader_config() {
        char s[512];
        int i, j;
        for (i = 0; i < active_shader->num_shaders; ++i) {
                struct glsl_shader *shader = &active_shader->shaders[i];
                char *name = shader->name;
                sprintf(s, "GL3 Shaders - %s", name);
                //                shader->shader_refresh_rate = config_get_float(CFG_MACHINE, s, "shader_refresh_rate", -1);
                for (j = 0; j < shader->num_parameters; ++j) {
                        struct shader_parameter *param = &shader->parameters[j];
                        param->value = config_get_float(CFG_MACHINE, s, param->id, param->default_value);
                }
        }
}

int gl3_init(SDL_Window *window, sdl_render_driver requested_render_driver, SDL_Rect screen) {
        int i, j;

        strcpy(current_render_driver_name, requested_render_driver.name);

        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
#ifdef __APPLE__
        // without an explicit request for the core profile 3.0 macOS falls back to default ancient 2.1
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif

        context = SDL_GL_CreateContext(window);

        if (!context) {
                pclog("Could not create GL context.\n");
                return FALSE;
        }

        SDL_GL_SetSwapInterval(video_vsync ? 1 : 0);

        pclog("OpenGL information: [%s] %s (%s)\n", glGetString(GL_VENDOR), glGetString(GL_RENDERER), glGetString(GL_VERSION));
        glsl_version[0] = glsl_version[1] = -1;
        glGetIntegerv(GL_MAJOR_VERSION, &glsl_version[0]);
        glGetIntegerv(GL_MINOR_VERSION, &glsl_version[1]);
        if (glsl_version[0] < 3) {
                pclog("OpenGL 3.0 is not available.");
                return SDL_FALSE;
        }
        pclog("Using OpenGL %s\n", glGetString(GL_VERSION));
        pclog("Using Shading Language %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));

        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
        pclog("Max texture size: %dx%d\n", max_texture_size, max_texture_size);

        // SDL_GL_MakeCurrent(window, context);

        glw = glw_init();

        glEnable(GL_TEXTURE_2D);

        scene_texture.data = NULL;
        scene_texture.width = screen.w;
        scene_texture.height = screen.h;
        scene_texture.internal_format = GL_RGBA8;
        scene_texture.format = GL_RGBA;
        scene_texture.type = GL_UNSIGNED_INT_8_8_8_8_REV;
        scene_texture.wrap_mode = GL_CLAMP_TO_BORDER;
        scene_texture.min_filter = scene_texture.mag_filter = video_scale_mode ? GL_LINEAR : GL_NEAREST;
        scene_texture.mipmap = 0;

        create_texture(&scene_texture);

        /* load shader */
        //        const char* shaders[1];
        //        shaders[0] = gl3_shader_file;
        //
        //        active_shader = load_shaders(1, shaders);

        //        const char* shaders[3];
        //        shaders[0] = "/home/phantasy/git/glsl-shaders/ntsc/ntsc-320px.glslp";
        //        shaders[1] = "/home/phantasy/git/glsl-shaders/motionblur/motionblur-simple.glslp";
        //        shaders[2] = "/home/phantasy/git/glsl-shaders/crt/crt-lottes-multipass.glslp";
        //
        //        active_shader = load_shaders(3, shaders);
        int num_shaders = 0;
        for (i = 0; i < MAX_USER_SHADERS; ++i) {
                if (strlen(gl3_shader_file[i]))
                        ++num_shaders;
                else
                        break;
        }
        active_shader = load_shaders(num_shaders, gl3_shader_file);

        create_scene_shader();

        /* read config */
        read_shader_config();

        /* buffers */

        GLfloat vertex[] = {-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f};

        GLfloat inv_vertex[] = {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f};

        GLfloat tex_coords[] = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f};

        GLfloat colors[] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};

        /* set the scene shader buffers */
        {
                glw->glBindVertexArray(active_shader->scene.vertex_array);

                struct shader_vbo *vbo = &active_shader->scene.vbo;

                glw->glGenBuffers(1, (GLuint *)&vbo->vertex_coord);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(inv_vertex), inv_vertex, GL_STATIC_DRAW);
                glw->glVertexAttribPointer(active_shader->scene.uniforms.vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
                                           (GLvoid *)0);

                glw->glGenBuffers(1, (GLuint *)&vbo->tex_coord);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->tex_coord);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
                glw->glVertexAttribPointer(active_shader->scene.uniforms.tex_coord, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat),
                                           (GLvoid *)0);
        }

        /* set buffers for all passes */
        for (j = 0; j < active_shader->num_shaders; ++j) {
                struct glsl_shader *shader = &active_shader->shaders[j];
                for (i = 0; i < shader->num_passes; ++i) {
                        struct shader_uniforms *u = &shader->passes[i].uniforms;

                        glw->glBindVertexArray(shader->passes[i].vertex_array);

                        struct shader_vbo *vbo = &shader->passes[i].vbo;

                        glw->glGenBuffers(1, (GLuint *)&vbo->vertex_coord);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
                        glw->glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

                        glw->glVertexAttribPointer(u->vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)0);

                        glw->glGenBuffers(1, (GLuint *)&vbo->tex_coord);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->tex_coord);
                        glw->glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
                        glw->glVertexAttribPointer(u->tex_coord, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat), (GLvoid *)0);

                        if (u->color) {
                                glw->glGenBuffers(1, (GLuint *)&vbo->color);
                                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->color);
                                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
                                glw->glVertexAttribPointer(u->color, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid *)0);
                        }
                }
        }

        for (i = 0; i < active_shader->num_shaders; ++i) {
                struct glsl_shader *shader = &active_shader->shaders[i];
                if (shader->has_prev) {
                        struct shader_pass *prev_pass = &shader->prev_scene;
                        create_default_shader_tex(prev_pass);

                        struct shader_vbo *vbo = &prev_pass->vbo;

                        glw->glBindVertexArray(prev_pass->vertex_array);

                        glw->glGenBuffers(1, (GLuint *)&vbo->vertex_coord);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
                        glw->glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
                        glw->glVertexAttribPointer(prev_pass->uniforms.vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
                                                   (GLvoid *)0);

                        glw->glGenBuffers(1, (GLuint *)&vbo->tex_coord);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->tex_coord);
                        glw->glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
                        glw->glVertexAttribPointer(prev_pass->uniforms.tex_coord, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat),
                                                   (GLvoid *)0);

                        for (j = 0; j < MAX_PREV; ++j) {
                                struct shader_prev *prev = &shader->prev[j];
                                struct shader_vbo *prev_vbo = &prev->vbo;

                                glw->glGenBuffers(1, (GLuint *)&prev_vbo->vertex_coord);
                                glw->glBindBuffer(GL_ARRAY_BUFFER, prev_vbo->vertex_coord);
                                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

                                glw->glGenBuffers(1, (GLuint *)&prev_vbo->tex_coord);
                                glw->glBindBuffer(GL_ARRAY_BUFFER, prev_vbo->tex_coord);
                                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
                        }
                }
        }

        /* create final pass */
        if (active_shader->num_shaders == 0 ||
            active_shader->shaders[active_shader->num_shaders - 1]
                            .passes[active_shader->shaders[active_shader->num_shaders - 1].num_passes - 1]
                            .fbo.id >= 0) {
                struct shader_pass *final_pass = &active_shader->final_pass;
                create_default_shader_tex(final_pass);

                glw->glBindVertexArray(final_pass->vertex_array);

                struct shader_vbo *vbo = &final_pass->vbo;

                glw->glGenBuffers(1, (GLuint *)&vbo->vertex_coord);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
                glw->glVertexAttribPointer(final_pass->uniforms.vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
                                           (GLvoid *)0);

                glw->glGenBuffers(1, (GLuint *)&vbo->tex_coord);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->tex_coord);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
                glw->glVertexAttribPointer(final_pass->uniforms.tex_coord, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat),
                                           (GLvoid *)0);
        }

        {
                struct shader_pass *color_pass = &active_shader->fs_color;
                create_default_shader_color(color_pass);

                glw->glBindVertexArray(color_pass->vertex_array);

                struct shader_vbo *vbo = &color_pass->vbo;

                glw->glGenBuffers(1, (GLuint *)&vbo->vertex_coord);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
                glw->glVertexAttribPointer(color_pass->uniforms.vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
                                           (GLvoid *)0);

                glw->glGenBuffers(1, (GLuint *)&vbo->color);
                glw->glBindBuffer(GL_ARRAY_BUFFER, vbo->color);
                glw->glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_DYNAMIC_DRAW);
                glw->glVertexAttribPointer(color_pass->uniforms.color, 4, GL_FLOAT, GL_TRUE, 4 * sizeof(GLfloat), (GLvoid *)0);
        }
#ifdef SDL2_SHADER_DEBUG
        struct shader_pass *debug_pass = &active_shader->debug;
        create_default_shader(debug_pass);

        glBindVertexArray(debug_pass->vertex_array);

        struct shader_vbo *vbo = &debug_pass->vbo;

        glGenBuffers(1, &vbo->vertex_coord);
        glBindBuffer(GL_ARRAY_BUFFER, vbo->vertex_coord);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
        glVertexAttribPointer(debug_pass->uniforms.vertex_coord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)0);

        glGenBuffers(1, &vbo->tex_coord);
        glBindBuffer(GL_ARRAY_BUFFER, vbo->tex_coord);
        glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_DYNAMIC_DRAW);
        glVertexAttribPointer(debug_pass->uniforms.tex_coord, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat), (GLvoid *)0);
#endif

        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);
        glw->glBindVertexArray(0);

        return SDL_TRUE;
}

void gl3_close() {
        if (context) {
                delete_texture(&scene_texture);

                if (active_shader) {
                        delete_glsl(active_shader);
                        free(active_shader);
                }
                active_shader = NULL;

                SDL_GL_DeleteContext(context);
                context = NULL;
        }
        glw_free(glw);
}

void gl3_update(SDL_Window *window, SDL_Rect updated_rect, BITMAP *screen) {
        if (!context)
                return;
        glBindTexture(GL_TEXTURE_2D, scene_texture.id);
        glPixelStorei(GL_UNPACK_ROW_LENGTH, screen->w);
        glTexSubImage2D(GL_TEXTURE_2D, 0, updated_rect.x, updated_rect.y, updated_rect.w, updated_rect.h, GL_BGRA,
                        GL_UNSIGNED_INT_8_8_8_8_REV, &((uint32_t *)screen->dat)[updated_rect.y * screen->w + updated_rect.x]);
        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
        glBindTexture(GL_TEXTURE_2D, 0);
}

struct render_data {
        int pass;
        struct glsl_shader *shader;
        struct shader_pass *shader_pass;
        GLfloat *output_size;
        struct shader_pass *orig_pass;
        GLint texture;
        int frame_count;
};

static void render_pass(struct render_data *data) {
        int i;
        GLuint texture_unit = 0;

        //        pclog("pass %d: %gx%g, %gx%g -> %gx%g, %gx%g, %gx%g\n", num_pass, pass->state.input_size[0],
        //        pass->state.input_size[1], pass->state.input_texture_size[0], pass->state.input_texture_size[1],
        //        pass->state.output_size[0], pass->state.output_size[1], pass->state.output_texture_size[0],
        //        pass->state.output_texture_size[1], output_size[0], output_size[1]);

        glw->glBindVertexArray(data->shader_pass->vertex_array);

        GLint p = data->shader_pass->program.id;
        struct shader_uniforms *u = &data->shader_pass->uniforms;

        glw->glUseProgram(p);

        if (data->texture) {
                glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                glBindTexture(GL_TEXTURE_2D, data->texture);
                glw->glUniform1i(u->texture, texture_unit);
                texture_unit++;
        }

        if (u->color >= 0)
                glw->glEnableVertexAttribArray(u->color);

        if (u->mvp_matrix >= 0)
                glw->glUniformMatrix4fv(u->mvp_matrix, 1, 0, matrix);
        if (u->frame_direction >= 0)
                glw->glUniform1i(u->frame_direction, 1);

        int framecnt = data->frame_count;
        if (data->shader_pass->frame_count_mod > 0)
                framecnt = framecnt % data->shader_pass->frame_count_mod;
        if (u->frame_count >= 0)
                glw->glUniform1i(u->frame_count, framecnt);

        if (u->input_size >= 0)
                glw->glUniform2fv(u->input_size, 1, data->shader_pass->state.input_size);
        if (u->texture_size >= 0)
                glw->glUniform2fv(u->texture_size, 1, data->shader_pass->state.input_texture_size);
        if (u->output_size >= 0)
                glw->glUniform2fv(u->output_size, 1, data->output_size);

        if (data->shader) {
                /* parameters */
                for (i = 0; i < data->shader->num_parameters; ++i)
                        if (u->parameters[i] >= 0)
                                glw->glUniform1f(u->parameters[i], data->shader->parameters[i].value);

                if (data->pass > 0) {
                        struct shader_pass *passes = data->shader->passes;
                        struct shader_pass *orig = data->orig_pass;
                        if (u->orig.texture >= 0) {
                                glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                                glBindTexture(GL_TEXTURE_2D, orig->fbo.texture.id);
                                glw->glUniform1i(u->orig.texture, texture_unit);
                                texture_unit++;
                        }
                        if (u->orig.input_size >= 0)
                                glw->glUniform2fv(u->orig.input_size, 1, orig->state.input_size);
                        if (u->orig.texture_size >= 0)
                                glw->glUniform2fv(u->orig.texture_size, 1, orig->state.input_texture_size);

                        for (i = 0; i < data->pass; ++i) {
                                if (u->pass[i].texture >= 0) {
                                        glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                                        glBindTexture(GL_TEXTURE_2D, passes[i].fbo.texture.id);
                                        glw->glUniform1i(u->pass[i].texture, texture_unit);
                                        texture_unit++;
                                }
                                if (u->pass[i].texture_size >= 0)
                                        glw->glUniform2fv(u->pass[i].texture_size, 1, passes[i].state.input_texture_size);
                                if (u->pass[i].input_size >= 0)
                                        glw->glUniform2fv(u->pass[i].input_size, 1, passes[i].state.input_size);

                                if (u->prev_pass[i].texture >= 0) {
                                        glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                                        glBindTexture(GL_TEXTURE_2D, passes[i].fbo.texture.id);
                                        glw->glUniform1i(u->prev_pass[i].texture, texture_unit);
                                        texture_unit++;
                                }
                                if (u->prev_pass[i].texture_size >= 0)
                                        glw->glUniform2fv(u->prev_pass[i].texture_size, 1, passes[i].state.input_texture_size);
                                if (u->prev_pass[i].input_size >= 0)
                                        glw->glUniform2fv(u->prev_pass[i].input_size, 1, passes[i].state.input_size);
                        }
                }

                if (data->shader->has_prev) {
                        /* loop through each previous frame */
                        for (i = 0; i < MAX_PREV; ++i) {
                                if (u->prev[i].texture >= 0) {
                                        glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                                        glBindTexture(GL_TEXTURE_2D, data->shader->prev[i].fbo.texture.id);
                                        glw->glUniform1i(u->prev[i].texture, texture_unit);
                                        texture_unit++;
                                }
                                if (u->prev[i].tex_coord >= 0) {
                                        glw->glBindBuffer(GL_ARRAY_BUFFER, data->shader->prev[i].vbo.tex_coord);
                                        glw->glVertexAttribPointer(u->prev[i].tex_coord, 2, GL_FLOAT, GL_TRUE,
                                                                   2 * sizeof(GLfloat), (GLvoid *)0);
                                        glw->glEnableVertexAttribArray(u->prev[i].tex_coord);
                                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);
                                }
                        }
                }

                for (i = 0; i < data->shader->num_lut_textures; ++i) {
                        if (u->lut_textures[i] >= 0) {
                                glw->glActiveTexture(GL_TEXTURE0 + texture_unit);
                                glBindTexture(GL_TEXTURE_2D, data->shader->lut_textures[i].texture.id);
                                glw->glUniform1i(u->lut_textures[i], texture_unit);
                                texture_unit++;
                        }
                }
        }

        glw->glEnableVertexAttribArray(u->vertex_coord);
        glw->glEnableVertexAttribArray(u->tex_coord);

        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        glw->glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, 0);

        glw->glDisableVertexAttribArray(data->shader_pass->uniforms.vertex_coord);
        glw->glDisableVertexAttribArray(data->shader_pass->uniforms.tex_coord);
        if (data->shader_pass->uniforms.color >= 0)
                glw->glDisableVertexAttribArray(data->shader_pass->uniforms.color);

        if (data->shader && data->shader->has_prev) {
                for (i = 0; i < MAX_PREV; ++i) {
                        if (u->prev[i].tex_coord >= 0)
                                glw->glDisableVertexAttribArray(u->prev[i].tex_coord);
                }
        }

        glw->glBindVertexArray(0);

        glw->glUseProgram(0);
}

void gl3_present(SDL_Window *window, SDL_Rect video_rect, SDL_Rect window_rect, SDL_Rect screen) {
        if (!context)
                return;

        int s, i, j;

        Uint32 ticks = SDL_GetTicks();

        GLfloat orig_output_size[] = {window_rect.w, window_rect.h};

        if (active_shader->srgb)
                glEnable(GL_FRAMEBUFFER_SRGB);

        struct render_data data;

        /* render scene to texture */
        {
                struct shader_pass *pass = &active_shader->scene;

                SDL_Rect rect;
                rect.x = rect.y = 0;
                rect.w = video_rect.w * gl3_input_scale;
                rect.h = video_rect.h * gl3_input_scale;
                sdl_scale(gl3_input_stretch, rect, &rect, video_rect.w, video_rect.h);

                pass->state.input_size[0] = pass->state.output_size[0] = rect.w;
                pass->state.input_size[1] = pass->state.output_size[1] = rect.h;

                pass->state.input_texture_size[0] = pass->state.output_texture_size[0] = next_pow2(pass->state.output_size[0]);
                pass->state.input_texture_size[1] = pass->state.output_texture_size[1] = next_pow2(pass->state.output_size[1]);

                recreate_fbo(&active_shader->scene.fbo, pass->state.output_texture_size[0], pass->state.output_texture_size[1]);

                glw->glBindFramebuffer(GL_FRAMEBUFFER, active_shader->scene.fbo.id);
                glClearColor(0, 0, 0, 1);
                glClear(GL_COLOR_BUFFER_BIT);

                glViewport(0, 0, pass->state.output_size[0], pass->state.output_size[1]);

                GLfloat minx = 0;
                GLfloat miny = 0;
                GLfloat maxx = pass->state.output_size[0] / (GLfloat)pass->state.output_texture_size[0];
                GLfloat maxy = pass->state.output_size[1] / (GLfloat)pass->state.output_texture_size[1];

                pass->state.tex_coords[0] = minx;
                pass->state.tex_coords[1] = miny;
                pass->state.tex_coords[2] = minx;
                pass->state.tex_coords[3] = maxy;
                pass->state.tex_coords[4] = maxx;
                pass->state.tex_coords[5] = miny;
                pass->state.tex_coords[6] = maxx;
                pass->state.tex_coords[7] = maxy;

                // create input tex coords
                minx = video_rect.x / (float)screen.w;
                miny = video_rect.y / (float)screen.h;
                maxx = (video_rect.x + video_rect.w) / (float)screen.w;
                maxy = (video_rect.y + video_rect.h) / (float)screen.h;

                GLfloat tex_coords[] = {minx, miny, minx, maxy, maxx, miny, maxx, maxy};

                glw->glBindVertexArray(pass->vertex_array);

                glw->glBindBuffer(GL_ARRAY_BUFFER, pass->vbo.tex_coord);
                glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(GLfloat), tex_coords);
                glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                memset(&data, 0, sizeof(struct render_data));
                data.pass = -1;
                data.shader_pass = &active_shader->scene;
                data.texture = scene_texture.id;
                data.output_size = orig_output_size;
                render_pass(&data);

                glw->glBindFramebuffer(GL_FRAMEBUFFER, 0);
        }

        struct shader_pass *orig = &active_shader->scene;
        struct shader_pass *input = &active_shader->scene;

        for (s = 0; s < active_shader->num_shaders; ++s) {
                struct glsl_shader *shader = &active_shader->shaders[s];

                //                float refresh_rate = shader->shader_refresh_rate;
                //                if (refresh_rate < 0)
                //                        refresh_rate = gl3_shader_refresh_rate;
                float refresh_rate = gl3_shader_refresh_rate;
                if (refresh_rate == 0)
                        refresh_rate = video_refresh_rate;
                int frame_count = ticks / (1000.0f / refresh_rate);

                /* loop through each pass */
                for (i = 0; i < shader->num_passes; ++i) {
                        struct shader_pass *pass = &shader->passes[i];

                        memcpy(pass->state.input_size, input->state.output_size, 2 * sizeof(GLfloat));
                        memcpy(pass->state.input_texture_size, input->state.output_texture_size, 2 * sizeof(GLfloat));

                        for (j = 0; j < 2; ++j) {
                                if (pass->scale.mode[j] == SCALE_VIEWPORT)
                                        pass->state.output_size[j] = orig_output_size[j] * pass->scale.value[j];
                                else if (pass->scale.mode[j] == SCALE_ABSOLUTE)
                                        pass->state.output_size[j] = pass->scale.value[j];
                                else
                                        pass->state.output_size[j] = pass->state.input_size[j] * pass->scale.value[j];

                                pass->state.output_texture_size[j] = next_pow2(pass->state.output_size[j]);
                        }

                        if (pass->fbo.id >= 0) {
                                recreate_fbo(&pass->fbo, pass->state.output_texture_size[0], pass->state.output_texture_size[1]);

                                glw->glBindFramebuffer(GL_FRAMEBUFFER, pass->fbo.id);
                                glViewport(0, 0, pass->state.output_size[0], pass->state.output_size[1]);
                        } else
                                glViewport(window_rect.x, window_rect.y, window_rect.w, window_rect.h);

                        glClearColor(0, 0, 0, 1);
                        glClear(GL_COLOR_BUFFER_BIT);

                        GLfloat minx = 0;
                        GLfloat miny = 0;
                        GLfloat maxx = pass->state.output_size[0] / (GLfloat)pass->state.output_texture_size[0];
                        GLfloat maxy = pass->state.output_size[1] / (GLfloat)pass->state.output_texture_size[1];

                        pass->state.tex_coords[0] = minx;
                        pass->state.tex_coords[1] = miny;
                        pass->state.tex_coords[2] = minx;
                        pass->state.tex_coords[3] = maxy;
                        pass->state.tex_coords[4] = maxx;
                        pass->state.tex_coords[5] = miny;
                        pass->state.tex_coords[6] = maxx;
                        pass->state.tex_coords[7] = maxy;

                        glw->glBindVertexArray(pass->vertex_array);

                        glw->glBindBuffer(GL_ARRAY_BUFFER, pass->vbo.tex_coord);
                        glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(GLfloat), input->state.tex_coords);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                        memset(&data, 0, sizeof(struct render_data));
                        data.shader = shader;
                        data.pass = i;
                        data.shader_pass = pass;
                        data.texture = input->fbo.texture.id;
                        data.output_size = orig_output_size;
                        data.orig_pass = orig;
                        data.frame_count = frame_count;

                        render_pass(&data);

                        glw->glBindFramebuffer(GL_FRAMEBUFFER, 0);

                        if (pass->fbo.texture.mipmap) {
                                glw->glActiveTexture(GL_TEXTURE0);
                                glBindTexture(GL_TEXTURE_2D, pass->fbo.texture.id);
                                glw->glGenerateMipmap(GL_TEXTURE_2D);
                                glBindTexture(GL_TEXTURE_2D, 0);
                        }

                        input = pass;
                }

                if (shader->has_prev && (ticks - shader->last_prev_update) >= (1000.f / refresh_rate)) {
                        shader->last_prev_update = ticks;

                        /* shift array */
                        memmove(&shader->prev[1], &shader->prev[0], MAX_PREV * sizeof(struct shader_prev));
                        memcpy(&shader->prev[0], &shader->prev[MAX_PREV], sizeof(struct shader_prev));

                        struct shader_pass *pass = orig;
                        struct shader_pass *prev_pass = &shader->prev_scene;
                        struct shader_prev *prev = &shader->prev[0];

                        memcpy(&prev_pass->state, &pass->state, sizeof(struct shader_state));

                        recreate_fbo(&prev->fbo, prev_pass->state.output_texture_size[0],
                                     prev_pass->state.output_texture_size[1]);

                        memcpy(&prev_pass->fbo, &prev->fbo, sizeof(struct shader_fbo));

                        glw->glBindFramebuffer(GL_FRAMEBUFFER, prev->fbo.id);
                        glClearColor(0, 0, 0, 1);
                        glClear(GL_COLOR_BUFFER_BIT);

                        glViewport(0, 0, pass->state.output_size[0], pass->state.output_size[1]);

                        glw->glBindVertexArray(prev_pass->vertex_array);

                        glw->glBindBuffer(GL_ARRAY_BUFFER, prev->vbo.tex_coord);
                        glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(GLfloat), pass->state.tex_coords);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                        glw->glBindBuffer(GL_ARRAY_BUFFER, prev_pass->vbo.tex_coord);
                        glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(GLfloat), pass->state.tex_coords);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                        memset(&data, 0, sizeof(struct render_data));
                        data.shader = shader;
                        data.pass = -10;
                        data.shader_pass = prev_pass;
                        data.texture = pass->fbo.texture.id;
                        data.output_size = orig_output_size;

                        render_pass(&data);

                        glw->glBindFramebuffer(GL_FRAMEBUFFER, 0);
                }

                orig = input;
        }

        if (active_shader->final_pass.active) {
                struct shader_pass *pass = &active_shader->final_pass;

                memcpy(pass->state.input_size, input->state.output_size, 2 * sizeof(GLfloat));
                memcpy(pass->state.input_texture_size, input->state.output_texture_size, 2 * sizeof(GLfloat));

                for (j = 0; j < 2; ++j) {
                        if (pass->scale.mode[j] == SCALE_VIEWPORT)
                                pass->state.output_size[j] = orig_output_size[j] * pass->scale.value[j];
                        else if (pass->scale.mode[j] == SCALE_ABSOLUTE)
                                pass->state.output_size[j] = pass->scale.value[j];
                        else
                                pass->state.output_size[j] = pass->state.input_size[j] * pass->scale.value[j];

                        pass->state.output_texture_size[j] = next_pow2(pass->state.output_size[j]);
                }

                glViewport(window_rect.x, window_rect.y, window_rect.w, window_rect.h);

                glClearColor(0, 0, 0, 1);
                glClear(GL_COLOR_BUFFER_BIT);

                GLfloat minx = 0;
                GLfloat miny = 0;
                GLfloat maxx = pass->state.output_size[0] / (GLfloat)pass->state.output_texture_size[0];
                GLfloat maxy = pass->state.output_size[1] / (GLfloat)pass->state.output_texture_size[1];

                pass->state.tex_coords[0] = minx;
                pass->state.tex_coords[1] = miny;
                pass->state.tex_coords[2] = minx;
                pass->state.tex_coords[3] = maxy;
                pass->state.tex_coords[4] = maxx;
                pass->state.tex_coords[5] = miny;
                pass->state.tex_coords[6] = maxx;
                pass->state.tex_coords[7] = maxy;

                glw->glBindVertexArray(pass->vertex_array);

                glw->glBindBuffer(GL_ARRAY_BUFFER, pass->vbo.tex_coord);
                glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(GLfloat), input->state.tex_coords);
                glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                memset(&data, 0, sizeof(struct render_data));
                data.pass = -2;
                data.shader_pass = pass;
                data.texture = input->fbo.texture.id;
                data.output_size = orig_output_size;
                data.orig_pass = orig;

                render_pass(&data);
        }

        if (!take_screenshot) {
                if (video_focus_dim && !(SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS)) {
                        struct shader_pass *pass = &active_shader->fs_color;
                        GLfloat r = 0;
                        GLfloat g = 0;
                        GLfloat b = 0;
                        GLfloat a = 0x80 / (float)0xff;

                        GLfloat colors[] = {r, g, b, a, r, g, b, a, r, g, b, a, r, g, b, a};

                        glw->glBindVertexArray(pass->vertex_array);

                        glw->glBindBuffer(GL_ARRAY_BUFFER, pass->vbo.color);
                        glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 16 * sizeof(GLfloat), colors);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                        memset(&data, 0, sizeof(struct render_data));
                        data.pass = -3;
                        data.shader_pass = pass;
                        data.texture = 0;
                        data.output_size = orig_output_size;
                        data.orig_pass = orig;

                        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
                        glEnable(GL_BLEND);
                        render_pass(&data);
                        glDisable(GL_BLEND);
                }
                if (flash.enabled) {
                        struct shader_pass *pass = &active_shader->fs_color;
                        GLfloat r = (flash.color[0] & 0xff) / (float)0xff;
                        GLfloat g = (flash.color[1] & 0xff) / (float)0xff;
                        GLfloat b = (flash.color[2] & 0xff) / (float)0xff;
                        GLfloat a = (flash.color[3] & 0xff) / (float)0xff;

                        GLfloat colors[] = {r, g, b, a, r, g, b, a, r, g, b, a, r, g, b, a};

                        glw->glBindVertexArray(pass->vertex_array);

                        glw->glBindBuffer(GL_ARRAY_BUFFER, pass->vbo.color);
                        glw->glBufferSubData(GL_ARRAY_BUFFER, 0, 16 * sizeof(GLfloat), colors);
                        glw->glBindBuffer(GL_ARRAY_BUFFER, 0);

                        memset(&data, 0, sizeof(struct render_data));
                        data.pass = -3;
                        data.shader_pass = pass;
                        data.texture = 0;
                        data.output_size = orig_output_size;
                        data.orig_pass = orig;

                        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
                        glEnable(GL_BLEND);
                        render_pass(&data);
                        glDisable(GL_BLEND);
                }
        } else {
                take_screenshot = 0;

                int width = window_rect.w;
                int height = window_rect.h;

                SDL_GetWindowSize(window, &width, &height);

                unsigned char *rgba = (unsigned char *)malloc(width * height * 4);

                glFinish();
                glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, rgba);

                int x, y;
                unsigned char *rgb = (unsigned char *)malloc(width * height * 3);

                for (x = 0; x < width; ++x) {
                        for (y = 0; y < height; ++y) {
                                rgb[(y * width + x) * 3 + 0] = rgba[((height - y - 1) * width + x) * 4 + 0];
                                rgb[(y * width + x) * 3 + 1] = rgba[((height - y - 1) * width + x) * 4 + 1];
                                rgb[(y * width + x) * 3 + 2] = rgba[((height - y - 1) * width + x) * 4 + 2];
                        }
                }

                screenshot_taken(rgb, width, height);

                free(rgb);
                free(rgba);
        }

        // DEBUG: render FBO
#ifdef SDL2_SHADER_DEBUG
        //        GLint texture = scene_texture.id;
        GLint texture = active_shader->lut_textures[0].texture.id;
        //        GLint texture = active_shader->scene.fbo.texture;

        glClearColor(1, 1, 1, 1);
        glClear(GL_COLOR_BUFFER_BIT);

        glViewport(0, 0, orig_output_size[0], orig_output_size[1]);

        struct shader_pass *pass = &active_shader->debug;

        render_pass(-999, pass, orig_output_size, texture);
#endif

        glDisable(GL_FRAMEBUFFER_SRGB);

        SDL_GL_SwapWindow(window);
}

sdl_renderer_t *gl3_renderer_create() {
        sdl_renderer_t *renderer = malloc(sizeof(sdl_renderer_t));
        renderer->init = gl3_init;
        renderer->close = gl3_close;
        renderer->update = gl3_update;
        renderer->present = gl3_present;
        renderer->always_update = 1;
        return renderer;
}

void gl3_renderer_close(sdl_renderer_t *renderer) { free(renderer); }

static int available = -1;

int gl3_renderer_available(struct sdl_render_driver *driver) {
        if (available < 0) {
                available = 0;

                // GL SetAttribute should be done *before* window creation for the attributes to apply on
                // context creation (seems to depend on OpenGL impl. but it souldn't hurt other platforms
                // to do it here (earlier than before)
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);

#ifdef __APPLE__
                // without an explicit request for the core profile 3.0 macOS falls back to default ancient 2.1
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif

                SDL_Window *window = SDL_CreateWindow("GL3 test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1, 1,
                                                      SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL);
                if (window) {
                        SDL_GLContext context = SDL_GL_CreateContext(window);
                        if (context) {
                                int version = -1;
                                glGetIntegerv(GL_MAJOR_VERSION, &version);

                                SDL_GL_DeleteContext(context);

                                available = version >= 3;
                        }
                        SDL_DestroyWindow(window);
                }
        }
        return available;
}
