#include #include #include #include #include #include "../include/chroma.h" #include "../include/stb_image.h" // Convert filter quality enum to OpenGL parameters static void get_gl_filter_params(chroma_filter_quality_t quality, GLint *min_filter, GLint *mag_filter, bool *needs_mipmaps) { switch (quality) { case CHROMA_FILTER_NEAREST: *min_filter = GL_NEAREST; *mag_filter = GL_NEAREST; *needs_mipmaps = false; break; case CHROMA_FILTER_LINEAR: *min_filter = GL_LINEAR; *mag_filter = GL_LINEAR; *needs_mipmaps = false; break; case CHROMA_FILTER_BILINEAR: *min_filter = GL_LINEAR; *mag_filter = GL_LINEAR; *needs_mipmaps = false; break; case CHROMA_FILTER_TRILINEAR: *min_filter = GL_LINEAR_MIPMAP_LINEAR; *mag_filter = GL_LINEAR; *needs_mipmaps = true; break; default: *min_filter = GL_LINEAR; *mag_filter = GL_LINEAR; *needs_mipmaps = false; break; } } // Calculate texture coordinates based on scaling mode and anchor position static void calculate_texture_coords(chroma_scale_mode_t scale_mode, float anchor_x, float anchor_y, int image_width, int image_height, int output_width, int output_height, float tex_coords[8]) { // Default texture coordinates (full texture) float u1 = 0.0f, v1 = 0.0f; // top-left float u2 = 1.0f, v2 = 1.0f; // bottom-right switch (scale_mode) { case CHROMA_SCALE_STRETCH: // Use full texture, stretch to fit u1 = 0.0f; v1 = 0.0f; u2 = 1.0f; v2 = 1.0f; break; case CHROMA_SCALE_CENTER: // Center image at original size // Calculate how much of the texture to show { float image_aspect = (float)image_width / image_height; float output_aspect = (float)output_width / output_height; if (image_aspect > output_aspect) { // Image is wider - fit width, show center portion vertically float visible_height = (float)image_width / output_aspect; float v_offset = (image_height - visible_height) / (2.0f * image_height); u1 = 0.0f; v1 = v_offset; u2 = 1.0f; v2 = 1.0f - v_offset; } else { // Image is taller - fit height, show center portion horizontally float visible_width = (float)image_height * output_aspect; float u_offset = (image_width - visible_width) / (2.0f * image_width); u1 = u_offset; v1 = 0.0f; u2 = 1.0f - u_offset; v2 = 1.0f; } } break; case CHROMA_SCALE_FIT: // Fit image within output, maintaining aspect ratio { float image_aspect = (float)image_width / image_height; float output_aspect = (float)output_width / output_height; if (image_aspect > output_aspect) { // Image is wider - fit width, add borders top/bottom float scaled_height = (float)output_width / image_aspect; float v_border = (output_height - scaled_height) / (2.0f * output_height); u1 = 0.0f; v1 = v_border; u2 = 1.0f; v2 = 1.0f - v_border; } else { // Image is taller - fit height, add borders left/right float scaled_width = (float)output_height * image_aspect; float u_border = (output_width - scaled_width) / (2.0f * output_width); u1 = u_border; v1 = 0.0f; u2 = 1.0f - u_border; v2 = 1.0f; } } break; case CHROMA_SCALE_FILL: default: // Fill entire output, crop if necessary { float image_aspect = (float)image_width / image_height; float output_aspect = (float)output_width / output_height; if (image_aspect > output_aspect) { // Image is wider - crop left/right float crop_width = image_height * output_aspect; float u_crop = (image_width - crop_width) / (2.0f * image_width); u1 = u_crop; v1 = 0.0f; u2 = 1.0f - u_crop; v2 = 1.0f; } else { // Image is taller - crop top/bottom float crop_height = image_width / output_aspect; float v_crop = (image_height - crop_height) / (2.0f * image_height); u1 = 0.0f; v1 = v_crop; u2 = 1.0f; v2 = 1.0f - v_crop; } } break; } // Apply anchor-based offset using anchor_x and anchor_y (0-100, 50=center) // anchor_x: 0=left edge, 50=center, 100=right edge // anchor_y: 0=top edge, 50=center, 100=bottom edge // u_shift: negative moves view left (shows more right side of image) // v_shift: negative moves view up (shows more top of image) float u_shift = (anchor_x - 50.0f) / 50.0f; // -1 to 1 float v_shift = (50.0f - anchor_y) / 50.0f; // -1 to 1 (inverted for top-down) // Calculate the shift amount based on crop/border space available float u_crop = 1.0f - (u2 - u1); float v_crop = 1.0f - (v2 - v1); u1 += u_shift * u_crop; u2 += u_shift * u_crop; v1 += v_shift * v_crop; v2 += v_shift * v_crop; // Clamp to valid range [0, 1] if (u1 < 0.0f) u1 = 0.0f; if (u2 > 1.0f) u2 = 1.0f; if (v1 < 0.0f) v1 = 0.0f; if (v2 > 1.0f) v2 = 1.0f; // Set texture coordinates for quad (bottom-left, bottom-right, top-right, // top-left) tex_coords[0] = u1; tex_coords[1] = v2; // bottom-left tex_coords[2] = u2; tex_coords[3] = v2; // bottom-right tex_coords[4] = u2; tex_coords[5] = v1; // top-right tex_coords[6] = u1; tex_coords[7] = v1; // top-left } // Vertex shader for simple texture rendering static const char *vertex_shader_source = "#version 120\n" "attribute vec2 position;\n" "attribute vec2 texcoord;\n" "varying vec2 v_texcoord;\n" "void main() {\n" " gl_Position = vec4(position, 0.0, 1.0);\n" " v_texcoord = texcoord;\n" "}\n"; // Fragment shader for simple texture rendering static const char *fragment_shader_source = "#version 120\n" "varying vec2 v_texcoord;\n" "uniform sampler2D texture;\n" "void main() {\n" " gl_FragColor = texture2D(texture, v_texcoord);\n" "}\n"; // Vertices for a fullscreen quad static const float vertices[] = { // Position Texcoord -1.0f, -1.0f, 0.0f, 1.0f, // bottom left 1.0f, -1.0f, 1.0f, 1.0f, // bottom right 1.0f, 1.0f, 1.0f, 0.0f, // top right -1.0f, 1.0f, 0.0f, 0.0f, // top left }; static const unsigned int indices[] = { 0, 1, 2, // first triangle 2, 3, 0 // second triangle }; static GLuint compile_shader(GLenum type, const char *source) { GLuint shader = glCreateShader(type); if (!shader) { chroma_log("ERROR", "Failed to create shader"); return 0; } glShaderSource(shader, 1, &source, NULL); glCompileShader(shader); GLint status; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (status != GL_TRUE) { char log[512]; glGetShaderInfoLog(shader, sizeof(log), NULL, log); chroma_log("ERROR", "Shader compilation failed: %s", log); glDeleteShader(shader); return 0; } return shader; } static GLuint create_shader_program(void) { GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source); if (!vertex_shader) { return 0; } GLuint fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source); if (!fragment_shader) { glDeleteShader(vertex_shader); return 0; } GLuint program = glCreateProgram(); if (!program) { chroma_log("ERROR", "Failed to create shader program"); glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return 0; } glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program); GLint status; glGetProgramiv(program, GL_LINK_STATUS, &status); if (status != GL_TRUE) { char log[512]; glGetProgramInfoLog(program, sizeof(log), NULL, log); chroma_log("ERROR", "Shader program linking failed: %s", log); glDeleteProgram(program); program = 0; } glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return program; } // Initialize OpenGL resources for output static int init_gl_resources(chroma_output_t *output) { if (!output || output->gl_resources_initialized) { return CHROMA_OK; } // Create shader prog output->shader_program = create_shader_program(); if (!output->shader_program) { chroma_log("ERROR", "Failed to create shader program for output %u", output->id); return CHROMA_ERROR_EGL; } // Create and setup VBO/EBO glGenBuffers(1, &output->vbo); glGenBuffers(1, &output->ebo); glBindBuffer(GL_ARRAY_BUFFER, output->vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); output->texture_id = 0; // will be created when image is assigned output->vbo_dirty = true; // VBO needs initial update output->gl_resources_initialized = true; chroma_log("DEBUG", "Initialized GL resources for output %u", output->id); return CHROMA_OK; } // Create or update texture from image data static int update_texture_from_image(chroma_output_t *output, chroma_image_t *image, chroma_filter_quality_t filter_quality) { if (!output || !image || !image->loaded) { return CHROMA_ERROR_INIT; } // If image data was already freed after previous GPU upload, we can't upload // again if (!image->data) { chroma_log("ERROR", "Cannot create texture: image data already freed for %s", image->path); return CHROMA_ERROR_IMAGE; } // Delete existing texture if it exists if (output->texture_id != 0) { // Estimate texture size for logging // FIXME: Unfortunately this only works if we have previous image info. // Could this b made more accurate? if (output->image && output->image->loaded) { size_t texture_size = (size_t)output->image->width * output->image->height * output->image->channels; chroma_log_resource_deallocation("gpu_texture", texture_size, "texture replacement"); } glDeleteTextures(1, &output->texture_id); output->texture_id = 0; } // Create new texture glGenTextures(1, &output->texture_id); glBindTexture(GL_TEXTURE_2D, output->texture_id); // Log GPU texture allocation size_t texture_size = (size_t)image->width * image->height * image->channels; chroma_log_resource_allocation("gpu_texture", texture_size, image->path); // Set texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Use configured filter quality GLint min_filter, mag_filter; bool needs_mipmaps; get_gl_filter_params(filter_quality, &min_filter, &mag_filter, &needs_mipmaps); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); // Upload texture data (always RGBA now) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->data); // Generate mipmaps for trilinear filtering if they're needed if (needs_mipmaps) { glGenerateMipmap(GL_TEXTURE_2D); chroma_log("DEBUG", "Generated mipmaps for trilinear filtering"); } glBindTexture(GL_TEXTURE_2D, 0); // Mark this output as having uploaded its texture output->texture_uploaded = true; // Free system RAM copy only when ALL outputs using this image have uploaded // to GPU if (image->data) { // Count total outputs using this image and how many have uploaded int total_using = 0; int uploaded_count = 0; chroma_state_t *state = output->state; for (int i = 0; i < state->output_count; i++) { if (state->outputs[i].active && state->outputs[i].image == image) { total_using++; if (state->outputs[i].texture_uploaded) { uploaded_count++; } } } // Only free image data when ALL outputs using it have uploaded if (total_using > 0 && uploaded_count >= total_using) { size_t freed_bytes = (size_t)image->width * image->height * image->channels; stbi_image_free(image->data); image->data = NULL; chroma_log("INFO", "Freed %.2f MB of image data after all %d outputs uploaded to " "GPU: %s", (double)freed_bytes / (1024.0 * 1024.0), total_using, image->path); chroma_log_resource_deallocation("image_data", freed_bytes, "post-gpu-upload"); chroma_log_memory_stats("post-gpu-upload"); } } chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id, image->width, image->height); return CHROMA_OK; } // Cleanup OpenGL resources for output static void cleanup_gl_resources(chroma_output_t *output) { if (!output || !output->gl_resources_initialized) { return; } if (output->texture_id != 0) { chroma_log_resource_deallocation("gpu_texture", 0, "cleanup"); glDeleteTextures(1, &output->texture_id); output->texture_id = 0; } if (output->shader_program != 0) { glDeleteProgram(output->shader_program); output->shader_program = 0; } if (output->vbo != 0) { glDeleteBuffers(1, &output->vbo); output->vbo = 0; } if (output->ebo != 0) { glDeleteBuffers(1, &output->ebo); output->ebo = 0; } output->gl_resources_initialized = false; chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id); } // EGL configuration selection static int choose_egl_config(EGLDisplay display, EGLConfig *config) { EGLint attributes[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE}; EGLint num_configs; if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) { chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError()); return -1; } if (num_configs == 0) { chroma_log("ERROR", "No suitable EGL configs found"); return -1; } return 0; } // EGL initialization int chroma_egl_init(chroma_state_t *state) { if (!state || !state->display) { return CHROMA_ERROR_INIT; } // Get EGL display state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display); if (state->egl_display == EGL_NO_DISPLAY) { chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError()); return CHROMA_ERROR_EGL; } // Initialize EGL EGLint major, minor; if (!eglInitialize(state->egl_display, &major, &minor)) { chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError()); return CHROMA_ERROR_EGL; } chroma_log("INFO", "EGL initialized: version %d.%d", major, minor); chroma_log_memory_stats("post-egl-init"); // Bind OpenGL API if (!eglBindAPI(EGL_OPENGL_API)) { chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError()); chroma_egl_cleanup(state); return CHROMA_ERROR_EGL; } // Choose EGL config if (choose_egl_config(state->egl_display, &state->egl_config) != 0) { chroma_egl_cleanup(state); return CHROMA_ERROR_EGL; } // Create EGL context EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE}; state->egl_context = eglCreateContext(state->egl_display, state->egl_config, EGL_NO_CONTEXT, context_attributes); if (state->egl_context == EGL_NO_CONTEXT) { chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError()); chroma_egl_cleanup(state); return CHROMA_ERROR_EGL; } chroma_log("INFO", "EGL context created successfully"); return CHROMA_OK; } // EGL cleanup void chroma_egl_cleanup(chroma_state_t *state) { if (!state) { return; } if (state->egl_display != EGL_NO_DISPLAY) { eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (state->egl_context != EGL_NO_CONTEXT) { eglDestroyContext(state->egl_display, state->egl_context); state->egl_context = EGL_NO_CONTEXT; } eglTerminate(state->egl_display); state->egl_display = EGL_NO_DISPLAY; } chroma_log("INFO", "EGL cleaned up"); } // Create surface for output int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) { if (!state || !output || !state->compositor || !state->layer_shell) { return CHROMA_ERROR_INIT; } // Create Wayland surface output->surface = wl_compositor_create_surface(state->compositor); if (!output->surface) { chroma_log("ERROR", "Failed to create Wayland surface for output %u", output->id); return CHROMA_ERROR_WAYLAND; } // Create layer surface for wallpaper output->layer_surface = zwlr_layer_shell_v1_get_layer_surface( state->layer_shell, output->surface, output->wl_output, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper"); if (!output->layer_surface) { chroma_log("ERROR", "Failed to create layer surface for output %u", output->id); chroma_surface_destroy(output); return CHROMA_ERROR_WAYLAND; } // Configure layer surface zwlr_layer_surface_v1_set_size(output->layer_surface, output->width, output->height); zwlr_layer_surface_v1_set_anchor(output->layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1); zwlr_layer_surface_v1_set_keyboard_interactivity( output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); // Add layer surface listener zwlr_layer_surface_v1_add_listener( output->layer_surface, &chroma_layer_surface_listener_impl, output); // Commit surface to trigger configure event wl_surface_commit(output->surface); // Wait for configure event wl_display_roundtrip(state->display); // Create EGL window output->egl_window = wl_egl_window_create(output->surface, output->width, output->height); if (!output->egl_window) { chroma_log("ERROR", "Failed to create EGL window for output %u", output->id); chroma_surface_destroy(output); return CHROMA_ERROR_EGL; } // Create EGL surface output->egl_surface = eglCreateWindowSurface(state->egl_display, state->egl_config, (EGLNativeWindowType)output->egl_window, NULL); if (output->egl_surface == EGL_NO_SURFACE) { chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x", output->id, eglGetError()); chroma_surface_destroy(output); return CHROMA_ERROR_EGL; } chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id, output->width, output->height); // Log surface creation resource allocation size_t surface_size = (size_t)output->width * output->height * 4; // estimate RGBA surface chroma_log_resource_allocation("egl_surface", surface_size, "output surface"); return CHROMA_OK; } // Destroy surface void chroma_surface_destroy(chroma_output_t *output) { if (!output) { return; } // Clean up OpenGL resources first cleanup_gl_resources(output); if (output->egl_surface != EGL_NO_SURFACE) { eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface); output->egl_surface = EGL_NO_SURFACE; } if (output->egl_window) { wl_egl_window_destroy(output->egl_window); output->egl_window = NULL; } if (output->layer_surface) { zwlr_layer_surface_v1_destroy(output->layer_surface); output->layer_surface = NULL; } if (output->surface) { wl_surface_destroy(output->surface); output->surface = NULL; } // Log surface destruction size_t surface_size = (size_t)output->width * output->height * 4; // estimate RGBA surface chroma_log_resource_deallocation("egl_surface", surface_size, "output surface cleanup"); chroma_log("DEBUG", "Destroyed surface for output %u", output->id); } // Render wallpaper to output using cached resources int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { if (!state || !output || !output->image || !output->image->loaded) { return CHROMA_ERROR_INIT; } // Make context current if (!eglMakeCurrent(state->egl_display, output->egl_surface, output->egl_surface, state->egl_context)) { chroma_log("ERROR", "Failed to make EGL context current: 0x%04x", eglGetError()); return CHROMA_ERROR_EGL; } if (init_gl_resources(output) != CHROMA_OK) { return CHROMA_ERROR_EGL; } if (output->texture_id == 0) { if (update_texture_from_image(output, output->image, output->filter_quality) != CHROMA_OK) { chroma_log("ERROR", "Failed to update texture for output %u", output->id); return CHROMA_ERROR_EGL; } } // Set viewport glViewport(0, 0, output->width, output->height); // Clear screen glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Use cached shader program glUseProgram(output->shader_program); // Bind cached texture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, output->texture_id); glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0); // Update VBO only if needed. E.g, image changed, scale mode changed, or first // render if (output->vbo_dirty) { // Calculate texture coordinates based on scaling mode, anchor, and anchor // coords float tex_coords[8]; calculate_texture_coords(output->scale_mode, output->anchor_x, output->anchor_y, output->image->width, output->image->height, output->width, output->height, tex_coords); // Create dynamic vertex data with calculated texture coordinates float dynamic_vertices[] = { // Position Texcoord -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left 1.0f, -1.0f, tex_coords[2], tex_coords[3], // bottom-right 1.0f, 1.0f, tex_coords[4], tex_coords[5], // top-right -1.0f, 1.0f, tex_coords[6], tex_coords[7] // top-left }; // Update VBO with dynamic texture coordinates glBindBuffer(GL_ARRAY_BUFFER, output->vbo); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices), dynamic_vertices); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); output->vbo_dirty = false; // mark VBO as up to date } else { // Just bind the existing buffers glBindBuffer(GL_ARRAY_BUFFER, output->vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); } // Set vertex attributes GLint position_attr = glGetAttribLocation(output->shader_program, "position"); GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord"); glEnableVertexAttribArray(position_attr); glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0); glEnableVertexAttribArray(texcoord_attr); glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float))); // Draw glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // Unbind resources glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); // Swap buffers if (!eglSwapBuffers(state->egl_display, output->egl_surface)) { chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x", output->id, eglGetError()); return CHROMA_ERROR_EGL; } // Commit surface wl_surface_commit(output->surface); chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)", output->id); return CHROMA_OK; } // Invalidate texture cache for output void chroma_output_invalidate_texture(chroma_output_t *output) { if (!output || !output->gl_resources_initialized) { return; } if (output->texture_id != 0) { glDeleteTextures(1, &output->texture_id); output->texture_id = 0; output->texture_uploaded = false; // reset upload flag chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id); } // Mark VBO as dirty since texture coordinates may need recalculation output->vbo_dirty = true; }