/* * 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); } }