summaryrefslogtreecommitdiff
path: root/platform_opengl.c
diff options
context:
space:
mode:
Diffstat (limited to 'platform_opengl.c')
-rw-r--r--platform_opengl.c541
1 files changed, 541 insertions, 0 deletions
diff --git a/platform_opengl.c b/platform_opengl.c
new file mode 100644
index 0000000..abe197a
--- /dev/null
+++ b/platform_opengl.c
@@ -0,0 +1,541 @@
+/*
+ * This is now a 7-pass shader
+ *
+ * 1. Phosphor persistence at source resolution (346×270)
+ * 2. Upscale and warp to viewport resolution without CRT effects → clean, sharp pixels
+ * 3. Extract bloom from upscaled clean pixels (white pixels, not RGB subpixels)
+ * 4. Blur the bloom vertically
+ * 5. Blur the bloom horizontally
+ * 6. CRT shader on original source with all the phosphor masks and scanlines
+ * 7. Composite warped bloom on top of the CRT effect
+ *
+ */
+
+#include "shader.c"
+#include "shader.h"
+
+INCBIN_SHADER_NOHEADER(vertex_shader, "#version 330", "shaders/gl_vertex.glsl");
+INCBIN_SHADER(fragment_shader, "#version 330", "shader.h", "shaders/gl_crt_fragment.glsl");
+INCBIN_SHADER_NOHEADER(phosphor_persistence_fragment, "#version 330", "shaders/gl_phosphor_persistence_fragment.glsl");
+INCBIN_SHADER_NOHEADER(upscale_warp_fragment, "#version 330", "shaders/gl_upscale_warp_fragment.glsl");
+INCBIN_SHADER_NOHEADER(bloom_extract_fragment, "#version 330", "shaders/gl_bloom_extract_fragment.glsl");
+INCBIN_SHADER_NOHEADER(bloom_blur_fragment, "#version 330", "shaders/gl_bloom_blur_fragment.glsl");
+INCBIN_SHADER_NOHEADER(bloom_composite_fragment, "#version 330", "shaders/gl_bloom_composite_fragment.glsl");
+
+// [=]===^=[ cleanup_render_targets ]=============================================================^===[=]
+static void cleanup_render_targets(void) {
+ // Don't delete state.texture or persistence textures here - they're managed by change_resolution()
+ glDeleteTextures(1, &state.crt_output_texture);
+ glDeleteTextures(1, &state.bloom_texture);
+ glDeleteTextures(1, &state.bloom_temp_texture);
+ glDeleteTextures(1, &state.bloom_warped_texture);
+ glDeleteTextures(1, &state.upscaled_source_texture);
+ glDeleteFramebuffers(1, &state.crt_fbo);
+ glDeleteFramebuffers(1, &state.bloom_fbo);
+ glDeleteFramebuffers(1, &state.bloom_temp_fbo);
+ glDeleteFramebuffers(1, &state.bloom_warp_fbo);
+ glDeleteFramebuffers(1, &state.upscaled_source_fbo);
+}
+
+// [=]===^=[ setup_render_target ]================================================================^===[=]
+static void setup_render_targets(void) {
+ cleanup_render_targets();
+
+ // Ensure source texture exists (might not if framebuffer_callback called before change_resolution)
+ if(state.texture == 0 && state.render_width > 0 && state.render_height > 0) {
+ GLfloat border_color[] = {0.0f, 0.0f, 0.0f, 1.0f};
+ glGenTextures(1, &state.texture);
+ glBindTexture(GL_TEXTURE_2D, state.texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ }
+
+ // Ensure persistence textures exist (created in change_resolution, not recreated on viewport change)
+ if(state.persistence_texture == 0 && state.render_width > 0 && state.render_height > 0) {
+ glGenTextures(1, &state.persistence_texture);
+ glBindTexture(GL_TEXTURE_2D, state.persistence_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glGenTextures(1, &state.persistence_output_texture);
+ glBindTexture(GL_TEXTURE_2D, state.persistence_output_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glGenFramebuffers(1, &state.persistence_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.persistence_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.persistence_output_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Persistence FBO not complete!\n");
+ }
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ }
+
+ // CRT output texture (full viewport resolution)
+ glGenTextures(1, &state.crt_output_texture);
+ glBindTexture(GL_TEXTURE_2D, state.crt_output_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.viewport.w, state.viewport.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ // Bloom textures (half resolution for wider blur spread)
+ state.bloom_width = state.viewport.w / 1;
+ state.bloom_height = state.viewport.h / 1;
+
+ glGenTextures(1, &state.bloom_texture);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, state.bloom_width, state.bloom_height, 0, GL_RGBA, GL_FLOAT, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ glGenTextures(1, &state.bloom_temp_texture);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_temp_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, state.bloom_width, state.bloom_height, 0, GL_RGBA, GL_FLOAT, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+ GLfloat black_border[] = {0.0f, 0.0f, 0.0f, 0.0f};
+ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, black_border);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ glGenTextures(1, &state.bloom_warped_texture);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_warped_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, state.bloom_width, state.bloom_height, 0, GL_RGBA, GL_FLOAT, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ // Create framebuffers
+ glGenFramebuffers(1, &state.crt_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.crt_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.crt_output_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("CRT FBO not complete!\n");
+ }
+
+ glGenFramebuffers(1, &state.bloom_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.bloom_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Bloom FBO not complete!\n");
+ }
+
+ glGenFramebuffers(1, &state.bloom_temp_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_temp_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.bloom_temp_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Bloom temp FBO not complete!\n");
+ }
+
+ glGenFramebuffers(1, &state.bloom_warp_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_warp_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.bloom_warped_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Bloom warp FBO not complete!\n");
+ }
+
+ // Upscaled source FBO (viewport resolution, for bloom extraction)
+ glGenTextures(1, &state.upscaled_source_texture);
+ glBindTexture(GL_TEXTURE_2D, state.upscaled_source_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.viewport.w, state.viewport.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ glGenFramebuffers(1, &state.upscaled_source_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.upscaled_source_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.upscaled_source_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Upscaled source FBO not complete!\n");
+ }
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+// [=]===^=[ change_resolution ]=================================================================^===[=]
+static void change_resolution(uint32_t new_width, uint32_t new_height) {
+ state.render_width = new_width;
+ state.render_height = new_height;
+
+ // Recreate source texture
+ GLfloat border_color[] = {0.0f, 0.0f, 0.0f, 1.0f};
+ glDeleteTextures(1, &state.texture);
+ glGenTextures(1, &state.texture);
+ glBindTexture(GL_TEXTURE_2D, state.texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Recreate persistence textures (tied to source resolution)
+ glDeleteTextures(1, &state.persistence_texture);
+ glDeleteTextures(1, &state.persistence_output_texture);
+ glDeleteFramebuffers(1, &state.persistence_fbo);
+
+ glGenTextures(1, &state.persistence_texture);
+ glBindTexture(GL_TEXTURE_2D, state.persistence_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glGenTextures(1, &state.persistence_output_texture);
+ glBindTexture(GL_TEXTURE_2D, state.persistence_output_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, state.render_width, state.render_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ glGenFramebuffers(1, &state.persistence_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, state.persistence_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, state.persistence_output_texture, 0);
+ if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ DEBUG_PRINT("Persistence FBO not complete!\n");
+ }
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+// [=]===^=[ compile_shader ]==============================================================^===[=]
+static GLuint compile_shader(GLenum shader_type, const char *shader_source) {
+ GLuint shader = glCreateShader(shader_type);
+ glShaderSource(shader, 1, &shader_source, 0);
+ glCompileShader(shader);
+
+ GLint success;
+ GLchar info_log[512];
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+ if(!success) {
+ glGetShaderInfoLog(shader, sizeof(info_log), 0, info_log);
+ DEBUG_PRINT("%s shader compilation failed:\n%s\n", (shader_type == GL_VERTEX_SHADER) ? "Vertex" : "Fragment", info_log);
+ }
+ return shader;
+}
+
+// [=]===^=[ opengl_setup ]================================================================^===[=]
+static void opengl_setup(void) {
+ gl_loader();
+ glEnable(GL_FRAMEBUFFER_SRGB);
+ glDisable(GL_DEPTH_TEST);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glDisable(GL_CULL_FACE);
+
+ // --- CRT Shader Setup ---
+ GLuint main_vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint main_fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_data);
+ state.shader_program = glCreateProgram();
+ glAttachShader(state.shader_program, main_vertex_shader);
+ glAttachShader(state.shader_program, main_fragment_shader);
+ glBindAttribLocation(state.shader_program, 0, "position");
+ glBindAttribLocation(state.shader_program, 1, "texture_coord");
+ glLinkProgram(state.shader_program);
+ glDeleteShader(main_vertex_shader);
+ glDeleteShader(main_fragment_shader);
+
+ // Initialize CRTS shader parameters
+ state.contrast = 1.0f;
+ state.saturation = 0.0f;
+ state.brightness = 1.0f;
+ CrtsTone(state.tone_data, state.contrast, state.saturation, INPUT_THIN, INPUT_MASK);
+
+ glUseProgram(state.shader_program);
+ state.uniform_resolution = glGetUniformLocation(state.shader_program, "resolution");
+ state.uniform_src_image_size = glGetUniformLocation(state.shader_program, "src_image_size");
+ state.uniform_brightness = glGetUniformLocation(state.shader_program, "brightness");
+ state.uniform_tone = glGetUniformLocation(state.shader_program, "tone_data");
+ state.uniform_crt_emulation = glGetUniformLocation(state.shader_program, "crt_emulation");
+ state.uniform_apply_mask = glGetUniformLocation(state.shader_program, "apply_mask");
+ state.uniform_sampler_location = glGetUniformLocation(state.shader_program, "iChannel0");
+
+ // --- Phosphor Persistence Shader Setup ---
+ GLuint persistence_vertex = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint persistence_fragment = compile_shader(GL_FRAGMENT_SHADER, phosphor_persistence_fragment_data);
+ state.persistence_program = glCreateProgram();
+ glAttachShader(state.persistence_program, persistence_vertex);
+ glAttachShader(state.persistence_program, persistence_fragment);
+ glBindAttribLocation(state.persistence_program, 0, "position");
+ glBindAttribLocation(state.persistence_program, 1, "texture_coord");
+ glLinkProgram(state.persistence_program);
+ glDeleteShader(persistence_vertex);
+ glDeleteShader(persistence_fragment);
+
+ // --- Upscale + Warp Shader Setup (for bloom path) ---
+ GLuint upscale_warp_vertex = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint upscale_warp_fragment = compile_shader(GL_FRAGMENT_SHADER, upscale_warp_fragment_data);
+ state.upscale_warp_program = glCreateProgram();
+ glAttachShader(state.upscale_warp_program, upscale_warp_vertex);
+ glAttachShader(state.upscale_warp_program, upscale_warp_fragment);
+ glBindAttribLocation(state.upscale_warp_program, 0, "position");
+ glBindAttribLocation(state.upscale_warp_program, 1, "texture_coord");
+ glLinkProgram(state.upscale_warp_program);
+ glDeleteShader(upscale_warp_vertex);
+ glDeleteShader(upscale_warp_fragment);
+
+ // --- Bloom Extract Shader Setup ---
+ GLuint bloom_extract_vertex = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint bloom_extract_fragment = compile_shader(GL_FRAGMENT_SHADER, bloom_extract_fragment_data);
+ state.bloom_extract_program = glCreateProgram();
+ glAttachShader(state.bloom_extract_program, bloom_extract_vertex);
+ glAttachShader(state.bloom_extract_program, bloom_extract_fragment);
+ glBindAttribLocation(state.bloom_extract_program, 0, "position");
+ glBindAttribLocation(state.bloom_extract_program, 1, "texture_coord");
+ glLinkProgram(state.bloom_extract_program);
+ glDeleteShader(bloom_extract_vertex);
+ glDeleteShader(bloom_extract_fragment);
+
+ glUseProgram(state.bloom_extract_program);
+ state.bloom_uniform_threshold = glGetUniformLocation(state.bloom_extract_program, "threshold");
+ state.bloom_uniform_sampler = glGetUniformLocation(state.bloom_extract_program, "source");
+
+ // --- Bloom Blur Shader Setup ---
+ GLuint bloom_blur_vertex = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint bloom_blur_fragment = compile_shader(GL_FRAGMENT_SHADER, bloom_blur_fragment_data);
+ state.bloom_blur_program = glCreateProgram();
+ glAttachShader(state.bloom_blur_program, bloom_blur_vertex);
+ glAttachShader(state.bloom_blur_program, bloom_blur_fragment);
+ glBindAttribLocation(state.bloom_blur_program, 0, "position");
+ glBindAttribLocation(state.bloom_blur_program, 1, "texture_coord");
+ glLinkProgram(state.bloom_blur_program);
+ glDeleteShader(bloom_blur_vertex);
+ glDeleteShader(bloom_blur_fragment);
+
+ glUseProgram(state.bloom_blur_program);
+ state.blur_uniform_horizontal = glGetUniformLocation(state.bloom_blur_program, "horizontal");
+ state.blur_uniform_sampler = glGetUniformLocation(state.bloom_blur_program, "source");
+
+ // --- Bloom Composite Shader Setup ---
+ GLuint bloom_composite_vertex = compile_shader(GL_VERTEX_SHADER, vertex_shader_data);
+ GLuint bloom_composite_fragment = compile_shader(GL_FRAGMENT_SHADER, bloom_composite_fragment_data);
+ state.bloom_composite_program = glCreateProgram();
+ glAttachShader(state.bloom_composite_program, bloom_composite_vertex);
+ glAttachShader(state.bloom_composite_program, bloom_composite_fragment);
+ glBindAttribLocation(state.bloom_composite_program, 0, "position");
+ glBindAttribLocation(state.bloom_composite_program, 1, "texture_coord");
+ glLinkProgram(state.bloom_composite_program);
+ glDeleteShader(bloom_composite_vertex);
+ glDeleteShader(bloom_composite_fragment);
+
+ glUseProgram(state.bloom_composite_program);
+ state.composite_uniform_bloom_strength = glGetUniformLocation(state.bloom_composite_program, "bloom_strength");
+ state.composite_uniform_crt_sampler = glGetUniformLocation(state.bloom_composite_program, "crt_texture");
+ state.composite_uniform_bloom_sampler = glGetUniformLocation(state.bloom_composite_program, "bloom_texture");
+
+ // Initialize bloom parameters
+ state.bloom_threshold = 0.8f; // Lower threshold = more pixels bloom
+ state.bloom_strength = 0.5f; // Higher strength = brighter bloom
+ state.persistence_decay = 0.02f; // 0.5 = subtle, 0.7 = more noticeable
+
+ glGenVertexArrays(1, &state.vao);
+ glGenBuffers(1, &state.vbo);
+ glGenBuffers(1, &state.ebo);
+ glBindVertexArray(state.vao);
+ const float vertices[] = {
+ -1.0f, -1.0f, 0.0f, 0.0f,
+ 1.0f, -1.0f, 1.0f, 0.0f,
+ 1.0f, 1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, 0.0f, 1.0f
+ };
+ static const unsigned int indices[] = { 0, 1, 2, 2, 3, 0 };
+ glBindBuffer(GL_ARRAY_BUFFER, state.vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, state.ebo);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+ glEnableVertexAttribArray(0);
+ glEnableVertexAttribArray(1);
+ glBindVertexArray(0);
+}
+
+// [=]===^=[ render_frame ]=================================================================^===[=]
+__attribute__((always_inline))
+static inline void render_frame(void) {
+ // Check if viewport changed and we need to recreate render targets
+ if(state.viewport_changed) {
+ setup_render_targets();
+ state.viewport_changed = 0;
+ }
+
+ // Upload new frame to texture
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.texture);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.render_width, state.render_height, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, display_buffer);
+
+ // Apply phosphor persistence (subtle trails on bright objects)
+ // Render: state.texture (current upload) + persistence_texture (previous) -> persistence_output_texture
+ glBindFramebuffer(GL_FRAMEBUFFER, state.persistence_fbo);
+ glViewport(0, 0, state.render_width, state.render_height);
+
+ glUseProgram(state.persistence_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.texture); // Current frame just uploaded
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, state.persistence_texture); // Previous frame
+ glUniform1i(glGetUniformLocation(state.persistence_program, "current_frame"), 0);
+ glUniform1i(glGetUniformLocation(state.persistence_program, "previous_frame"), 1);
+ glUniform1f(glGetUniformLocation(state.persistence_program, "decay"), state.persistence_decay);
+
+ glBindVertexArray(state.vao);
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // Copy output back to persistence_texture for next frame
+ glBindTexture(GL_TEXTURE_2D, state.persistence_texture);
+ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, state.render_width, state.render_height);
+
+ // Use persistence output as source for rest of rendering pipeline
+ GLuint source_texture = state.persistence_output_texture;
+
+ if(state.toggle_bloom) {
+ // ========== PASS 1: Upscale + Warp source (matches CRT geometry) ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, state.upscaled_source_fbo);
+ glViewport(0, 0, state.viewport.w, state.viewport.h);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.upscale_warp_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, source_texture);
+ // Use linear filtering for upscale+warp to reduce aliasing in bloom
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glUniform1i(glGetUniformLocation(state.upscale_warp_program, "source"), 0);
+ glUniform2f(glGetUniformLocation(state.upscale_warp_program, "resolution"), (float)state.viewport.w, (float)state.viewport.h);
+ glUniform2f(glGetUniformLocation(state.upscale_warp_program, "src_image_size"), (float)state.render_width, (float)state.render_height);
+
+ glBindVertexArray(state.vao);
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // Restore nearest filtering for CRT shader
+ glBindTexture(GL_TEXTURE_2D, source_texture);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // ========== PASS 2: Extract bright pixels from upscaled source ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_temp_fbo);
+ glViewport(0, 0, state.bloom_width, state.bloom_height);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.bloom_extract_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.upscaled_source_texture);
+ glUniform1f(state.bloom_uniform_threshold, state.bloom_threshold);
+ glUniform1i(state.bloom_uniform_sampler, 0);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // ========== PASS 3: Horizontal blur ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_fbo);
+ glViewport(0, 0, state.bloom_width, state.bloom_height);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.bloom_blur_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_temp_texture);
+ glUniform1i(state.blur_uniform_horizontal, 1);
+ glUniform1i(state.blur_uniform_sampler, 0);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // ========== PASS 4: Vertical blur ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, state.bloom_temp_fbo);
+ glViewport(0, 0, state.bloom_width, state.bloom_height);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.bloom_blur_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_texture);
+ glUniform1i(state.blur_uniform_horizontal, 0);
+ glUniform1i(state.blur_uniform_sampler, 0);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // ========== PASS 5: CRT Shader to offscreen FBO ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, state.crt_fbo);
+ glViewport(0, 0, state.viewport.w, state.viewport.h);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.shader_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, source_texture);
+ glUniform2f(state.uniform_src_image_size, (float)state.render_width, (float)state.render_height);
+ glUniform2f(state.uniform_resolution, (float)state.viewport.w, (float)state.viewport.h);
+ glUniform1f(state.uniform_brightness, state.brightness);
+ glUniform4f(state.uniform_tone, state.tone_data[0], state.tone_data[1], state.tone_data[2], state.tone_data[3]);
+ glUniform1i(state.uniform_crt_emulation, state.toggle_crt_emulation);
+ glUniform1i(state.uniform_apply_mask, 0); // Don't apply mask - composite will do it
+ glUniform1i(state.uniform_sampler_location, 0);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ // ========== PASS 6: Composite CRT + Bloom to screen ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glViewport(state.viewport.x, state.viewport.y, state.viewport.w, state.viewport.h);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.bloom_composite_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, state.crt_output_texture);
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, state.bloom_temp_texture);
+ glUniform1f(state.composite_uniform_bloom_strength, state.bloom_strength);
+ glUniform1i(state.composite_uniform_crt_sampler, 0);
+ glUniform1i(state.composite_uniform_bloom_sampler, 1);
+ glUniform2f(glGetUniformLocation(state.bloom_composite_program, "bloom_size"), (float)state.bloom_width, (float)state.bloom_height);
+ glUniform2f(glGetUniformLocation(state.bloom_composite_program, "src_image_size"), (float)state.render_width, (float)state.render_height);
+ glUniform2f(glGetUniformLocation(state.bloom_composite_program, "viewport_size"), (float)state.viewport.w, (float)state.viewport.h);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ glBindVertexArray(0);
+ glUseProgram(0);
+
+ } else {
+ // ========== Single pass: CRT Shader directly to screen ==========
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glViewport(state.viewport.x, state.viewport.y, state.viewport.w, state.viewport.h);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ glUseProgram(state.shader_program);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, source_texture);
+ glUniform2f(state.uniform_src_image_size, (float)state.render_width, (float)state.render_height);
+ glUniform2f(state.uniform_resolution, (float)state.viewport.w, (float)state.viewport.h);
+ glUniform1f(state.uniform_brightness, state.brightness);
+ glUniform4f(state.uniform_tone, state.tone_data[0], state.tone_data[1], state.tone_data[2], state.tone_data[3]);
+ glUniform1i(state.uniform_crt_emulation, state.toggle_crt_emulation);
+ glUniform1i(state.uniform_apply_mask, 1); // Apply mask in CRT shader when bloom disabled
+ glUniform1i(state.uniform_sampler_location, 0);
+
+ glBindVertexArray(state.vao);
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ glBindVertexArray(0);
+ glUseProgram(0);
+ }
+} \ No newline at end of file